Compare commits

...

95 Commits

Author SHA1 Message Date
Fabian Schlenz 94785357bc
Updated README.md to reflect the state of the project. 2020-04-27 18:32:37 +02:00
Fabian Schlenz 223a0fdde3 Added blacklist_extensions to skip some file types completely from being downloaded. 2018-04-20 06:54:29 +02:00
Fabian Schlenz bce5996643 Show (temporary) messages every time a FLOOD_WAIT is happening. 2018-04-20 06:53:25 +02:00
Fabian Schlenz cf806da77d Added spinners to visualize the ongoing download of bigger media files. 2018-04-20 06:52:30 +02:00
Fabian Schlenz c2b1c0625e Playing around with exporters. Added a stupidly simple CSV exporter. 2018-04-20 06:43:48 +02:00
Fabian Schlenz 3c68e6d814 Merge branch 'feature-json2' 2018-04-20 06:11:11 +02:00
Fabian Schlenz 7e2d49ef09 Added a setting max_file_size to ignore files bigger than that size in MB. 2018-04-20 06:08:17 +02:00
Fabian Schlenz 11a6318a26 JSON objects now also contain a property api_layer containing their (surprise!) api_layer. 2018-04-19 17:48:11 +02:00
Fabian Schlenz bb2a291d4f MediaFileManagers now use JSON instead of the TLMessage objects. 2018-04-17 06:36:39 +02:00
Fabian Schlenz b1e9346203 Anonymize "target dir after options". 2018-04-16 05:59:28 +02:00
Fabian Schlenz 6b5b9a669b WIP: Reworking mediafilemanagers to work with JSON. 2018-04-16 05:57:05 +02:00
Fabian Schlenz 6b9cc9533a Better status reports from the DatabaseUpdate. 2018-04-14 15:01:54 +02:00
Fabian Schlenz 4c6c049502 Removed duplicate headers printed while downloading media. 2018-04-13 06:18:20 +02:00
Fabian Schlenz 28b402d3ab deploy.sh and deploy_bet.sh will now print short git diff stats into the telegram group's message. 2018-04-13 06:17:33 +02:00
Fabian Schlenz 6d4701189b Added json columns to users and chats tables; newly incoming objects are now saved together with their JSON data. 2018-04-13 06:10:27 +02:00
Fabian Schlenz c963a8f334 deploy.sh: Go back to master after merging into stable. 2018-04-12 06:55:06 +02:00
Fabian Schlenz 2402356013 Added a DatabaseUpdate to convert the data to json. 2018-04-12 06:54:14 +02:00
Fabian Schlenz df1c90578b Disabled version check if version number contains a dash. 2018-04-12 06:47:24 +02:00
Fabian Schlenz 96d5b5c77b DownloadManager now looks at all dialogs, not just the last 100. 2018-04-12 06:46:42 +02:00
Fabian Schlenz c5f901f4a2 Small codefix to use parameter instead of fixed value. 2018-04-12 05:57:11 +02:00
Fabian Schlenz e8c28b4e72 `--limit-messages` now also affects channels and supergroups. 2018-04-12 05:56:30 +02:00
Fabian Schlenz f434482cdf Database connection will now always be closed on VM shutdown. 2018-04-12 05:55:45 +02:00
Fabian Schlenz 4d45c7f1cc Copy config.sample.ini to the right folder. 2018-04-11 06:26:05 +02:00
Fabian Schlenz 955ae42952 Use exit code 1 if quitting because of an exception. 2018-04-11 06:24:59 +02:00
Fabian Schlenz 01590b05ee Reformatted the exporter selection part of CommandLineController. 2018-04-11 06:24:24 +02:00
Fabian Schlenz b5ff4dd1a6 Removed debug output from CommandLineOptions. 2018-04-11 06:16:42 +02:00
Fabian Schlenz b199dc335f Queries in Database now go through a custom function executeQuery which throws better (read: informative) Exceptions. 2018-04-11 06:14:55 +02:00
Fabian Schlenz 1ff540977e Merge branch 'feature-rewrite' 2018-04-11 06:11:07 +02:00
Fabian Schlenz ff9163c1bb Settings: Is CLI is set, use it instead of INI. 2018-04-11 05:51:00 +02:00
Fabian Schlenz 38fce0ee5c Some more refactoring of Settings. 2018-04-11 05:50:26 +02:00
Fabian Schlenz f24e66271f Added command '--settings', which will print the currently active settings. Settings can now be marked as secret - if that is true, the settings value will only be printed if it is not the default value. 2018-04-10 06:38:28 +02:00
Fabian Schlenz a9444e7813 Rewrote CommandLineOptions: The code is now better readable and accepts parameters like `--target=/data` and accepts some short parameters like -t for --target. 2018-04-10 06:36:18 +02:00
Fabian Schlenz 069799cbaf Resorted the code in DownloadManager - no more need for downloadMessages calling _downloadMessages or downloadMedia calling _downloadMedia. 2018-04-10 06:33:15 +02:00
Fabian Schlenz 9affb47130 Reworked the obeyFloodLimit-Stuff to now use lambdas. 2018-04-10 06:26:46 +02:00
Fabian Schlenz be1cf8ba91 Fixed a bug in Utils.anonymize 2018-04-10 06:13:29 +02:00
Fabian Schlenz 5c466131d3 Rewritten Settings. 2018-04-10 06:12:02 +02:00
Fabian Schlenz e5da546386 CommandLineOptions will now insert booleans into values with an value of "true" as well. 2018-04-10 06:11:01 +02:00
Fabian Schlenz ec4097e777 The code is now compiling, but still largely untested. So it's still kinda WIP. 2018-04-09 06:07:53 +02:00
Fabian Schlenz 77efde1136 WIP: Still having fun with git stash. -.- 2018-04-06 06:21:39 +02:00
Fabian Schlenz 253b334fc3 More rewriting. Also lots of fun with git stash and wrong branches... 2018-04-06 06:19:49 +02:00
Fabian Schlenz ebff71b208 WIP: Even moar rewriting. 2018-03-20 06:44:36 +01:00
Fabian Schlenz eea08a5559 WIP: Lots and lots of rewriting. 2018-03-16 06:59:18 +01:00
Fabian Schlenz 78031b0ff2 Don't download media files in daemon mode if download_media is false. 2018-03-15 20:49:37 +01:00
Fabian Schlenz f4d563226c Added limit and offset parameters to Database#getMessagesWithMedia in order to prevent high memory usage in downloadMedia. 2018-03-15 20:49:15 +01:00
Fabian Schlenz 968ee831f0 Added max_file_age to download only newer files. 2018-03-15 06:48:37 +01:00
Fabian Schlenz 2d409352bc WIP: Some more rewriting. 2018-03-14 06:09:24 +01:00
Fabian Schlenz 97cf26b46d WIP: Started rewriting the code to make it more null-safe. 2018-03-14 06:08:07 +01:00
Fabian Schlenz 6276651b84 Fixed anonymization of database backup debug messages. Fixes #98. 2018-03-13 06:46:59 +01:00
Fabian Schlenz f8984b25b1 Renamed IniSettings#get to IniSettings#getString. 2018-03-13 06:44:33 +01:00
Fabian Schlenz 2295ced528 Fixed a bug in IniSettings that prevented new accounts from being created. 2018-03-13 06:42:39 +01:00
Fabian Schlenz aec609e6c4 Merge branch 'feature-settings' 2018-03-13 06:32:06 +01:00
Fabian Schlenz dd99612bed Added command line switch `--list-channels` to list channels and supergroups with their ID to add them to black- and whitelists in config.ini 2018-03-13 06:31:38 +01:00
Fabian Schlenz 25a01fae4b You can now restrict the downloading of channels and supergroups by defining black- and whitelists in config.ini 2018-03-13 06:30:39 +01:00
Fabian Schlenz 077cbcebca deploy_beta.sh: Make clear that the changelog is since the last real release. 2018-03-13 06:10:44 +01:00
Fabian Schlenz a8149dfce9 deploy_beta.sh: Fixed a line causing the script to wait for STDIN (don't really understand, why) and modified the "this is just for testing, backup your data" warning. 2018-03-13 06:09:52 +01:00
Fabian Schlenz ecb225ef60 Extended .gitignore. 2018-03-12 22:02:43 +01:00
Fabian Schlenz c79336618c deploy_beta.sh: A small script to deploy the current beta version to the telegram group. 2018-03-12 22:02:14 +01:00
Fabian Schlenz e75aa2101e Fixed lots of unclosed ResultSets in Database and DatabaseUpdates. 2018-03-12 22:01:47 +01:00
Fabian Schlenz 19973818f8 Moved more settings to IniSettings. 2018-03-10 23:26:03 +01:00
Fabian Schlenz d796cb1bf0 deploy.sh: Telegram message is now HTML formatted. 2018-03-08 23:11:48 +01:00
Fabian Schlenz b0fa297a61 deploy.sh: Moved the gradle build task to earlier in the process to better be able to catch errors. 2018-03-08 23:11:14 +01:00
Fabian Schlenz a8944125b6 deploy.sh: Check out master after running the tool. 2018-03-08 22:32:51 +01:00
Fabian Schlenz d66834c3d5 deploy.sh: Use the html_url of the new release. 2018-03-08 22:32:18 +01:00
Fabian Schlenz c99766a71e deploy.sh: Create relases as published, not as draft. 2018-03-08 22:31:27 +01:00
Fabian Schlenz 79b68bd93d deploy.sh: Don't fail if the Dockerfile can't be committed (possibly because it already had the correct version in it). 2018-03-08 22:23:20 +01:00
Fabian Schlenz dcdc313c8b Merge branch 'master' of https://github.com/fabianonline/telegram_backup 2018-03-08 22:21:39 +01:00
Fabian Schlenz ac85f06e3e Bumping the version to 1.1.3 2018-03-08 22:21:11 +01:00
Fabian Schlenz 65ae4f4a86 Fixed typo in deploy.sh 2018-03-08 22:21:07 +01:00
Fabian Schlenz 9f9d9fd183 Fixed a source for non-closed PreparedStatements. 2018-03-08 22:17:01 +01:00
Fabian Schlenz c29cd2a8ee A DatabaseUpdate can now contain a List of create_query-Strings that are used if the database is to be created from scratch. 2018-03-08 22:15:49 +01:00
Fabian Schlenz 68e5c9be2d If a new version is available, the message will be displayed a bit more prominent and with a 5 second delay. 2018-03-08 22:13:30 +01:00
Fabian Schlenz 9a9e4284d9 Modified deploy.sh to set the tag on master, not on stable, so that `git describe` for versions built from master gives usable output. 2018-03-08 22:12:29 +01:00
Fabian Schlenz 3c5a8e9d38 Deployment can not happen automatically via deploy.sh 2018-03-08 06:56:41 +01:00
Fabian Schlenz 6c6725e711
Merge pull request #51 from orschiro/patch-1
Corrects the path given in README.md.
2018-03-07 18:07:32 +01:00
Fabian Schlenz 834aaf0292 Extended .gitignore 2018-03-07 18:05:02 +01:00
Fabian Schlenz 365e38970d Modified the Dockerfile to make a bit smaller images. 2018-03-07 18:04:25 +01:00
Fabian Schlenz e40096fc44 Removed the deploy command from .travis.yml 2018-03-07 18:03:56 +01:00
Fabian Schlenz b65e624876 Merge branch 'master' of https://github.com/fabianonline/telegram_backup 2018-03-07 18:02:58 +01:00
Fabian Schlenz 85c525ab1c Newsly download GMaps images have a red marker in them to pinpoint the exact location. 2018-03-07 17:15:59 +01:00
Fabian Schlenz da2a7d88b6 Geo Images are now shown in the HTML export. Fixes #95. 2018-03-07 17:15:20 +01:00
Fabian Schlenz b959c35bea Usind `--login --account <x>` will now directly login you to the specified account. Fixes #78. 2018-03-07 06:15:16 +01:00
Fabian Schlenz 89cca39409 IniSettings will be initialized at startup. 2018-03-07 06:12:37 +01:00
Fabian Schlenz 7fa89ab1b1 IniSettings class is now finished and also already used for GMAPS_KEY. 2018-03-07 06:09:51 +01:00
Fabian Schlenz 53fcd36e66 config.sample.ini is copied into the data directory on every run. 2018-03-07 09:48:23 +01:00
Fabian Schlenz 897435adf9
Merge pull request #77 from jakevossen5/master
Typo: Capitalized Month in README.md
2018-03-06 20:59:27 +01:00
Fabian Schlenz 74dbc9d412 Commit 0e2eeab accidentally removed the line in `--help` mentioning `--daemon`. Fixed that. 2018-03-06 06:30:50 +01:00
Fabian Schlenz 7b49153c93 Now catching (and printing) ALL exceptions in CommandLineController. 2018-03-06 06:28:37 +01:00
Fabian Schlenz 07572c0618 Extended IniSettings to be able to parse ini files. Yay. But: Still WIP! 2018-03-06 06:27:31 +01:00
Fabian Schlenz e3aaa58256 Small changes to DB_Update_9: Progress report, ORDER for the query, closing ResultSets. 2018-03-06 06:20:02 +01:00
Fabian Schlenz 9678aaaee8 Started IniSettings. (WIP!) 2018-03-05 18:26:53 +01:00
Fabian Schlenz 4e74b4c30b Renamed Settings.kt to DbSettings.kt 2018-03-05 06:56:38 +01:00
Fabian Schlenz 75786e39b4 Modified Settings class to only represent internal Settings. Also, renamed it to DbSettings. 2018-03-05 06:55:24 +01:00
Fabian Schlenz 42dc500514 Added Settings. 2018-02-22 07:55:53 +01:00
Jake Vossen 52d90ffc43
Capitalized Month 2017-12-27 20:39:31 -07:00
Robert Orzanna 7dd0c044cd Use hidden folder on Linux 2017-07-09 10:22:08 +02:00
42 changed files with 2240 additions and 1771 deletions

4
.gitignore vendored
View File

@ -10,3 +10,7 @@ src/main/main.iml
cache4.*
src/test/test.iml
dev/
todo
deploy.secret.sh
release_notes.txt
out

View File

@ -11,13 +11,3 @@ cache:
directories:
- $HOME/.gradle/caches/
- $HOME/.gradle/wrapper/
deploy:
provider: pages
skip-cleanup: true
github-token: $github_token
keep-history: true
on:
branch: master
local-dir: build/libs
target-branch: gh-pages

View File

@ -2,6 +2,7 @@
* Update the version in the Dockerfile to the coming version.
* Commit the new Dockerfile.
* Merge into stable: `git checkout stable && git merge --no-ff master`
* Create a new tag for the new version: `git tag -a <version>`.
* Push everything to github: `git push --all && git push --tags`.
* Build it: `gradle build`.

View File

@ -1,10 +1,15 @@
FROM openjdk:8
ENV JAR_VERSION 1.1.2
ENV JAR_VERSION 1.1.3
ENV JAR_DOWNLOAD_URL https://github.com/fabianonline/telegram_backup/releases/download/${JAR_VERSION}/telegram_backup.jar
RUN apt-get update -y && apt-get install -y curl && \
RUN apt-get update -y && \
apt-get install --no-install-recommends -y curl && \
curl -L "https://github.com/Yelp/dumb-init/releases/download/v1.1.3/dumb-init_1.1.3_amd64" -o /bin/dumb-init && \
curl -L $JAR_DOWNLOAD_URL -o telegram_backup.jar && mkdir /data/ && chmod +x /bin/dumb-init
curl -L $JAR_DOWNLOAD_URL -o telegram_backup.jar && mkdir /data/ && \
chmod +x /bin/dumb-init && \
apt-get remove -y curl && \
apt-get autoremove -y && \
rm -rf /var/lib/apt/lists/*
ENTRYPOINT ["/bin/dumb-init", "--", "java", "-jar", "telegram_backup.jar", "--target", "/data/"]

View File

@ -2,6 +2,16 @@
Copyright 2016 Fabian Schlenz
Licensed under GPLv3
## State of this project
The tool is working, but not really as intended: Media files in most cases can't be downloaded, message downloads are hit with 30 second delays after every 200 messages. Some users reported getting banned by Telegram without reason after using this tool (with many users not getting banned at the same time, so this could theoretically just be a coincidence).
At the same time, the official Telegram client has an official way to download one's data, which is a) officially supported and b) much, much, much faster than this tool.
Fixing this tool to at least get it to work again as planned would require more or less a complete rewrite of this code. Since I'm quite happy with the possibilities given by the official clients and don't have enough free time to spare to continue developing this project, I've decided to officially archive this tool. This is not an easy step for me, because this was my most used project and quite a lot of people wrote me nice messages and thanked me. But just keeping the user's hopes up for an update without really being able to do something doesn't seem fair. So...
So long, and thanks for all the fish. ;-) \
Fabian
## Description
This is a small Java app that allows you to download all your history from
Telegram's servers and keep a local copy of them.
@ -21,7 +31,7 @@ You can find the whole app packed into one fat jar file under
## Limitations
This tool relies on Telegram's API. They started rate limiting the calls
made by this tool some time ago. As of february 2017, downloading messages
made by this tool some time ago. As of February 2017, downloading messages
is limited to 400 messages every 30 seconds, resulting in 48,000 messages
per hour. Media download is not throttled right now, so it should be a lot
quicker.
@ -85,7 +95,7 @@ this will be detected at the next run of this program and then tried again.
The files are being saved in your User directory in a folder named
`telegram_backup`. Under windows, this would typically be under
`C:\Users\<username>\telegram_backup`. Linux users should look unter
`/home/<username>/telegram_backup`.
`/home/<username>/.telegram_backup`.
You can change this directory by supplying `--target <dir>` when calling
Telegram_Backup.

View File

@ -38,7 +38,8 @@ dependencies {
compile 'com.github.spullara.mustache.java:compiler:0.9.5'
compile 'org.slf4j:slf4j-api:1.7.21'
compile 'ch.qos.logback:logback-classic:1.1.7'
compile 'com.google.code.gson:gson:2.5'
compile 'com.google.code.gson:gson:2.8.0'
compile 'com.github.salomonbrys.kotson:kotson:2.5.0'
compile 'com.github.kittinunf.fuel:fuel:1.12.0'
testCompile 'junit:junit:4.12'

93
deploy.sh Executable file
View File

@ -0,0 +1,93 @@
#!/bin/bash
error() {
echo "Error: $1"
exit 1
}
[ -z "$1" ] && error "Parameter's missing. Expecting version number like '1.2.3' as first and only parameter."
if [ "$1" == "--help" ]; then
echo "Usage: `basename "$0"` 1.2.3"
exit 1
fi
release_notes="$(cat release_notes.txt 2>/dev/null)"
[ -z "$release_notes" ] && error "release_notes.txt is empty"
VERSION="$1"
source "deploy.secret.sh"
[ -z "$BOT_TOKEN" ] && error "BOT_TOKEN is not set or empty."
[ -z "$CHAT_ID" ] && error "CHAT_ID is not set or empty."
[ -z "$TOKEN" ] && error "TOKEN is not set or empty."
CURL_OPTS="-u fabianonline:$TOKEN"
git diff-files --quiet --ignore-submodules -- || error "You have changes in your working tree."
git diff-index --cached --quiet HEAD --ignore-submodules -- || error "You have uncommited changes."
branch_name=$(git symbolic-ref HEAD 2>/dev/null)
branch_name=${branch_name##refs/heads/}
[ "$branch_name" == "master" ] || error "Current branch is $branch_name, not master."
echo "Updating the Dockerfile..."
sed -i "s/ENV JAR_VERSION .\+/ENV JAR_VERSION $VERSION/g" Dockerfile || error "Couldn't modify Dockerfile."
echo "Committing the new Dockerfile..."
git commit -m "Bumping the version to $VERSION" Dockerfile
echo "Tagging the new version..."
git tag -a "$VERSION" -m "Version $VERSION" || error
echo "Building it..."
gradle build || error "Build failed. What did you do?!"
echo "Getting git stats..."
git_stats=$(git diff --shortstat stable..)
echo "Checking out stable..."
git checkout stable || error
echo "Merging master into stable..."
git merge --no-ff -m "Merging master into stable for version $VERSION" master || error
echo "Checking out master again..."
git checkout master || error
echo "Pushing all to Github..."
git push --all || error
echo "Pushing tags to Github..."
git push --tags || error
echo "Generating a release on Github..."
json=$(ruby -e "require 'json'; puts({tag_name: '$VERSION', name: '$VERSION', body: \$stdin.read}.to_json)" <<< "$release_notes") || error "Couldn't generate JSON for Github"
json=$(curl $CURL_OPTS https://api.github.com/repos/fabianonline/telegram_backup/releases -XPOST -d "$json") || error "Github failure"
echo "Uploading telegram_backup.jar to Github..."
upload_url=$(jq -r ".upload_url" <<< "$json") || error "Could not parse JSON from Github"
upload_url=$(sed 's/{.*}//' <<< "$upload_url")
release_url=$(jq -r ".html_url" <<< "$json") || error "Could not parse JSON from Github"
curl $CURL_OPTS --header "Content-Type: application/zip" "${upload_url}?name=telegram_backup.jar" --upload-file build/libs/telegram_backup.jar || error "Asset upload to github failed"
echo "Building the docker image..."
docker build -t fabianonline/telegram_backup:$VERSION -t fabianonline/telegram_backup:latest - < Dockerfile
echo "Pushing the docker image..."
docker push fabianonline/telegram_backup
echo "Notifying the Telegram group..."
release_notes=$(sed 's/\* /• /' | sed 's/&/&amp;/g' | sed 's/</\&lt;/g' | sed 's/>/\&gt;/g' <<< "$release_notes")
message="<b>Version $VERSION was just released</b>"$'\n'"$git_stats"$'\n'$'\n'"$release_notes"$'\n'$'\n'"$release_url"
curl https://api.telegram.org/bot${BOT_TOKEN}/sendMessage -XPOST --form "text=<-" --form-string "chat_id=${CHAT_ID}" --form-string "parse_mode=HTML" --form-string "disable_web_page_preview=true" <<< "$message"
echo "Cleaning release_notes.txt..."
> release_notes.txt
echo "Checking out master..."
git checkout master
echo "Done."

44
deploy_beta.sh Executable file
View File

@ -0,0 +1,44 @@
#!/bin/bash
error() {
echo "Error: $1"
exit 1
}
release_notes="$(cat release_notes.txt 2>/dev/null)"
source "deploy.secret.sh"
[ -z "$BOT_TOKEN" ] && error "BOT_TOKEN is not set or empty."
[ -z "$CHAT_ID" ] && error "CHAT_ID is not set or empty."
version=$(git describe --tags --dirty)
echo "Enter additional notes, end with Ctrl-D."
additional_notes="$(cat)"
echo "Building it..."
gradle build || error "Build failed. What did you do?!"
echo "Getting git stats..."
git_stats=$(git diff --shortstat stable..)
echo "Copying it to files.fabianonline.de..."
filename="telegram_backup.beta_${version}.jar"
cp --no-preserve "mode,ownership,timestamps" build/libs/telegram_backup.jar /data/containers/nginx/www/files/${filename}
echo "Notifying the Telegram group..."
release_notes=$(echo "$release_notes" | sed 's/\* /• /' | sed 's/&/&amp;/g' | sed 's/</\&lt;/g' | sed 's/>/\&gt;/g')
message="<b>New beta release $version</b>"$'\n'
message="${message}${git_stats}"$'\n\n'
message="${message}${additional_notes}"$'\n\n'
message="${message}Changes since the last <i>real</i> release:"$'\n'"${release_notes}"$'\n\n'
message="${message}<b>This is a release for testing purposes only. There may be bugs included that might destroy your data. Only use this beta release if you know what you're doing. AND MAKE A BACKUP OF YOUR BACKUP BEFORE USING IT!</b>"$'\n\n'
message="${message}Please report back if you used this release and encountered a bug. Also report back, if you used it and IT WORKED, please. Thank you."$'\n\n'
message="${message}https://files.fabianonline.de/${filename}"
result=$(curl https://api.telegram.org/bot${BOT_TOKEN}/sendMessage -XPOST --form "text=<-" --form-string "chat_id=${CHAT_ID}" --form-string "parse_mode=HTML" --form-string "disable_web_page_preview=true" <<< "$message")
message_id=$(jq -r '.result.message_id' <<< "$result")
echo "Pinning the new message..."
curl https://api.telegram.org/bot${BOT_TOKEN}/pinChatMessage -XPOST --form "chat_id=${CHAT_ID}" --form "message_id=${message_id}" --form "disable_notification=true"
echo "Done."

View File

@ -1,4 +1,4 @@
#Thu Oct 06 11:24:39 CEST 2016
#Fri Mar 23 06:07:28 CET 2018
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME

View File

@ -0,0 +1,5 @@
package de.fabianonline.telegram_backup
class Account(val file_base: String, val phone_number: String) {
}

View File

@ -22,120 +22,64 @@ import com.github.badoualy.telegram.mtproto.auth.AuthKey
import com.github.badoualy.telegram.mtproto.model.MTSession
import org.apache.commons.io.FileUtils
import org.slf4j.LoggerFactory
import org.slf4j.Logger
import java.io.File
import java.io.FileNotFoundException
import java.io.IOException
internal class ApiStorage(prefix: String?) : TelegramApiStorage {
private var prefix: String? = null
private var do_save = false
private var auth_key: AuthKey? = null
private var dc: DataCenter? = null
private var file_auth_key: File? = null
private var file_dc: File? = null
internal class ApiStorage(val base_dir: String) : TelegramApiStorage {
var auth_key: AuthKey? = null
var dc: DataCenter? = null
val file_auth_key: File
val file_dc: File
val logger = LoggerFactory.getLogger(ApiStorage::class.java)
init {
this.setPrefix(prefix)
}
fun setPrefix(prefix: String?) {
this.prefix = prefix
this.do_save = this.prefix != null
if (this.do_save) {
val base = Config.FILE_BASE +
File.separatorChar +
this.prefix +
File.separatorChar
this.file_auth_key = File(base + Config.FILE_NAME_AUTH_KEY)
this.file_dc = File(base + Config.FILE_NAME_DC)
this._saveAuthKey()
this._saveDc()
} else {
this.file_auth_key = null
this.file_dc = null
}
file_auth_key = File(base_dir + Config.FILE_NAME_AUTH_KEY)
file_dc = File(base_dir + Config.FILE_NAME_DC)
}
override fun saveAuthKey(authKey: AuthKey) {
this.auth_key = authKey
this._saveAuthKey()
}
private fun _saveAuthKey() {
if (this.do_save && this.auth_key != null) {
try {
FileUtils.writeByteArrayToFile(this.file_auth_key, this.auth_key!!.key)
} catch (e: IOException) {
e.printStackTrace()
}
}
FileUtils.writeByteArrayToFile(file_auth_key, authKey.key)
}
override fun loadAuthKey(): AuthKey? {
if (this.auth_key != null) return this.auth_key
if (this.file_auth_key != null) {
try {
return AuthKey(FileUtils.readFileToByteArray(this.file_auth_key))
} catch (e: IOException) {
if (e !is FileNotFoundException) e.printStackTrace()
}
try {
return AuthKey(FileUtils.readFileToByteArray(file_auth_key))
} catch (e: FileNotFoundException) {
return null
}
return null
}
override fun saveDc(dataCenter: DataCenter) {
this.dc = dataCenter
this._saveDc()
}
private fun _saveDc() {
if (this.do_save && this.dc != null) {
try {
FileUtils.write(this.file_dc, this.dc!!.toString())
} catch (e: IOException) {
e.printStackTrace()
}
}
FileUtils.write(file_dc, dataCenter.toString())
}
override fun loadDc(): DataCenter? {
if (this.dc != null) return this.dc
if (this.file_dc != null) {
try {
val infos = FileUtils.readFileToString(this.file_dc).split(":")
return DataCenter(infos[0], Integer.parseInt(infos[1]))
} catch (e: IOException) {
if (e !is FileNotFoundException) e.printStackTrace()
}
try {
val infos = FileUtils.readFileToString(this.file_dc).split(":")
return DataCenter(infos[0], Integer.parseInt(infos[1]))
} catch (e: FileNotFoundException) {
return null
}
return null
}
override fun deleteAuthKey() {
if (this.do_save) {
try {
FileUtils.forceDelete(this.file_auth_key)
} catch (e: IOException) {
e.printStackTrace()
}
try {
FileUtils.forceDelete(file_auth_key)
} catch (e: IOException) {
logger.warn("Exception in deleteAuthKey(): {}", e)
}
}
override fun deleteDc() {
if (this.do_save) {
try {
FileUtils.forceDelete(this.file_dc)
} catch (e: IOException) {
e.printStackTrace()
}
try {
FileUtils.forceDelete(file_dc)
} catch (e: IOException) {
logger.warn("Exception in deleteDc(): {}", e)
}
}

View File

@ -16,7 +16,7 @@
package de.fabianonline.telegram_backup
import de.fabianonline.telegram_backup.TelegramUpdateHandler
import de.fabianonline.telegram_backup.exporter.HTMLExporter
import de.fabianonline.telegram_backup.exporter.*
import com.github.badoualy.telegram.api.Kotlogram
import com.github.badoualy.telegram.api.TelegramApp
import com.github.badoualy.telegram.api.TelegramClient
@ -29,132 +29,174 @@ import java.util.HashMap
import org.slf4j.LoggerFactory
import org.slf4j.Logger
class CommandLineController {
private val storage: ApiStorage
var app: TelegramApp
private fun getLine(): String {
if (System.console() != null) {
return System.console().readLine("> ")
} else {
print("> ")
return Scanner(System.`in`).nextLine()
}
}
private fun getPassword(): String {
if (System.console() != null) {
return String(System.console().readPassword("> "))
} else {
return getLine()
}
}
class CommandLineController(val options: CommandLineOptions) {
val logger = LoggerFactory.getLogger(CommandLineController::class.java)
init {
val storage: ApiStorage
val app: TelegramApp
val target_dir: String
val file_base: String
val phone_number: String
val handler: TelegramUpdateHandler
var client: TelegramClient
val user_manager: UserManager
val settings: Settings
val database: Database
logger.info("CommandLineController started. App version {}", Config.APP_APPVER)
this.printHeader()
if (CommandLineOptions.cmd_version) {
printHeader()
if (options.isSet("version")) {
System.exit(0)
} else if (CommandLineOptions.cmd_help) {
this.show_help()
} else if (options.isSet("help")) {
show_help()
System.exit(0)
} else if (CommandLineOptions.cmd_license) {
CommandLineController.show_license()
System.exit(0)
}
this.setupFileBase()
if (CommandLineOptions.cmd_list_accounts) {
this.list_accounts()
} else if (options.isSet("license")) {
show_license()
System.exit(0)
}
// Setup TelegramApp
logger.debug("Initializing TelegramApp")
app = TelegramApp(Config.APP_ID, Config.APP_HASH, Config.APP_MODEL, Config.APP_SYSVER, Config.APP_APPVER, Config.APP_LANG)
// Setup file_base
logger.debug("Target dir from Config: {}", Config.TARGET_DIR.anonymize())
target_dir = options.get("target") ?: Config.TARGET_DIR
logger.debug("Target dir after options: {}", target_dir.anonymize())
println("Base directory for files: ${target_dir.anonymize()}")
if (options.isSet("list_accounts")) {
Utils.print_accounts(target_dir)
System.exit(0)
}
if (options.isSet("login")) {
cmd_login(app, target_dir, options.get("account"))
}
logger.trace("Checking accounts")
val account = this.selectAccount()
logger.debug("CommandLineOptions.cmd_login: {}", CommandLineOptions.cmd_login)
phone_number = try { selectAccount(target_dir, options.get("account"))
} catch(e: AccountNotFoundException) {
show_error("The specified account could not be found.")
} catch(e: NoAccountsException) {
println("No accounts found. Starting login process...")
cmd_login(app, target_dir, options.get("account"))
}
// TODO: Create a new TelegramApp if the user set his/her own TelegramApp credentials
// At this point we can assume that the selected user account ("phone_number") exists.
// So we can create some objects:
file_base = build_file_base(target_dir, phone_number)
logger.info("Initializing ApiStorage")
storage = ApiStorage(account)
logger.info("Initializing TelegramUpdateHandler")
val handler = TelegramUpdateHandler()
storage = ApiStorage(file_base)
logger.info("Creating Client")
val client = Kotlogram.getDefaultClient(app, storage, Kotlogram.PROD_DC4, handler)
client = Kotlogram.getDefaultClient(app, storage, Kotlogram.PROD_DC4, null)
// From now on we have a new catch-all-block that will terminate it's TelegramClient when an exception happens.
try {
logger.info("Initializing UserManager")
UserManager.init(client)
val user = UserManager.getInstance()
if (!CommandLineOptions.cmd_login && !user.loggedIn) {
user_manager = UserManager(client)
// TODO
/*if (!options.cmd_login && !user.loggedIn) {
println("Your authorization data is invalid or missing. You will have to login with Telegram again.")
CommandLineOptions.cmd_login = true
}
if (account != null && user.loggedIn) {
if (account != "+" + user.user!!.getPhone()) {
logger.error("Account: {}, user.user!!.getPhone(): +{}", account.anonymize(), user.user!!.getPhone().anonymize())
throw RuntimeException("Account / User mismatch")
}
}
logger.debug("CommandLineOptions.cmd_login: {}", CommandLineOptions.cmd_login)
if (CommandLineOptions.cmd_login) {
cmd_login(account)
System.exit(0)
options.cmd_login = true
}*/
if (phone_number != user_manager.phone) {
logger.error("phone_number: {}, user_manager.phone: {}", phone_number.anonymize(), user_manager.phone.anonymize())
show_error("Account / User mismatch")
}
// If we reach this point, we can assume that there is an account and a database can be loaded / created.
Database.init(client)
if (CommandLineOptions.cmd_stats) {
cmd_stats()
database = Database(file_base, user_manager)
Runtime.getRuntime().addShutdownHook(Thread() {
database.close()
})
// Load the settings and stuff.
settings = Settings(file_base, database, options)
if (options.isSet("stats")) {
cmd_stats(file_base, database)
System.exit(0)
} else if (options.isSet("settings")) {
settings.print()
System.exit(0)
}
if (CommandLineOptions.val_test != null) {
if (CommandLineOptions.val_test == 1) {
TestFeatures.test1()
} else if (CommandLineOptions.val_test == 2) {
TestFeatures.test2()
} else {
System.out.println("Unknown test " + CommandLineOptions.val_test)
}
System.exit(1)
}
logger.debug("CommandLineOptions.val_export: {}", CommandLineOptions.val_export)
if (CommandLineOptions.val_export != null) {
if (CommandLineOptions.val_export!!.toLowerCase().equals("html")) {
(HTMLExporter()).export()
System.exit(0)
} else {
show_error("Unknown export format.")
}
}
if (user.loggedIn) {
System.out.println("You are logged in as ${user.userString.anonymize()}")
} else {
println("You are not logged in.")
System.exit(1)
val export = options.get("export")?.toLowerCase()
logger.debug("options.export: {}", export)
when(export) {
"html" -> { HTMLExporter(database, user_manager, settings=settings, file_base=file_base).export() ; System.exit(0) }
"csv" -> { CSVExporter(database, file_base, settings).export(); System.exit(0) }
"csv_links" -> { CSVLinkExporter(database, file_base, settings).export() ; System.exit(0) }
null -> { /* No export whished -> do nothing. */ }
else -> show_error("Unknown export format '${export}'.")
}
println("You are logged in as ${user_manager.toString().anonymize()}")
logger.info("Initializing Download Manager")
val d = DownloadManager(client, CommandLineDownloadProgress())
logger.debug("Calling DownloadManager.downloadMessages with limit {}", CommandLineOptions.val_limit_messages)
d.downloadMessages(CommandLineOptions.val_limit_messages)
logger.debug("CommandLineOptions.cmd_no_media: {}", CommandLineOptions.cmd_no_media)
if (!CommandLineOptions.cmd_no_media) {
val d = DownloadManager(client, CommandLineDownloadProgress(), database, user_manager, settings, file_base)
if (options.isSet("list_channels")) {
val chats = d.getChats()
val print_header = {download: Boolean -> println("%-15s %-40s %s".format("ID", "Title", if (download) "Download" else "")); println("-".repeat(65)) }
val format = {c: DownloadManager.Channel, download: Boolean -> "%-15s %-40s %s".format(c.id.toString().anonymize(), c.title.anonymize(), if (download) (if(c.download) "YES" else "no") else "")}
var download: Boolean
println("Channels:")
download = settings.download_channels
if (!download) println("Download of channels is disabled - see download_channels in config.ini")
print_header(download)
for (c in chats.channels) {
println(format(c, download))
}
println()
println("Supergroups:")
download = settings.download_supergroups
if (!download) println("Download of supergroups is disabled - see download_supergroups in config.ini")
print_header(download)
for (c in chats.supergroups) {
println(format(c, download))
}
System.exit(0)
}
logger.debug("Calling DownloadManager.downloadMessages with limit {}", options.get("limit_messages"))
d.downloadMessages(options.get("limit_messages")?.toInt())
logger.debug("IniSettings#download_media: {}", settings.download_media)
if (settings.download_media) {
logger.debug("Calling DownloadManager.downloadMedia")
d.downloadMedia()
} else {
println("Skipping media download because --no-media is set.")
println("Skipping media download because download_media is set to false.")
}
} catch (e: Exception) {
if (options.isSet("daemon")) {
logger.info("Initializing TelegramUpdateHandler")
handler = TelegramUpdateHandler(user_manager, database, file_base, settings)
client.close()
logger.info("Creating new client")
client = Kotlogram.getDefaultClient(app, storage, Kotlogram.PROD_DC4, handler)
println("DAEMON mode requested - keeping running.")
}
} catch (e: Throwable) {
println("An error occurred!")
e.printStackTrace()
logger.error("Exception caught!", e)
// If we encountered an exception, we definitely don't want to start the daemon mode now.
CommandLineOptions.cmd_daemon = false
System.exit(1)
} finally {
if (CommandLineOptions.cmd_daemon) {
handler.activate()
println("DAEMON mode requested - keeping running.")
} else {
client.close()
println()
println("----- EXIT -----")
System.exit(0)
}
client.close()
println()
println("----- EXIT -----")
System.exit(0)
}
}
@ -166,153 +208,103 @@ class CommandLineController {
println()
}
private fun setupFileBase() {
logger.debug("Target dir at startup: {}", Config.FILE_BASE.anonymize())
if (CommandLineOptions.val_target != null) {
Config.FILE_BASE = CommandLineOptions.val_target!!
}
logger.debug("Target dir after options: {}", Config.FILE_BASE.anonymize())
System.out.println("Base directory for files: " + Config.FILE_BASE.anonymize())
}
private fun selectAccount(): String? {
var account = "none"
val accounts = Utils.getAccounts()
if (CommandLineOptions.cmd_login) {
logger.debug("Login requested, doing nothing.")
// do nothing
} else if (CommandLineOptions.val_account != null) {
logger.debug("Account requested: {}", CommandLineOptions.val_account!!.anonymize())
private fun selectAccount(file_base: String, requested_account: String?): String {
var found_account: String?
val accounts = Utils.getAccounts(file_base)
if (requested_account != null) {
logger.debug("Account requested: {}", requested_account.anonymize())
logger.trace("Checking accounts for match.")
var found = false
for (acc in accounts) {
logger.trace("Checking {}", acc.anonymize())
if (acc == CommandLineOptions.val_account) {
found = true
logger.trace("Matches.")
break
}
}
if (!found) {
show_error("Couldn't find account '" + CommandLineOptions.val_account!!.anonymize() + "'. Maybe you want to use '--login' first?")
}
account = CommandLineOptions.val_account!!
found_account = accounts.find{it == requested_account}
} else if (accounts.size == 0) {
println("No accounts found. Starting login process...")
CommandLineOptions.cmd_login = true
return null
throw NoAccountsException()
} else if (accounts.size == 1) {
account = accounts.firstElement()
System.out.println("Using only available account: " + account.anonymize())
found_account = accounts.firstElement()
println("Using only available account: " + found_account.anonymize())
} else {
show_error(("You didn't specify which account to use.\n" +
show_error(("You have more than one account but didn't specify which one to use.\n" +
"Use '--account <x>' to use account <x>.\n" +
"Use '--list-accounts' to see all available accounts."))
System.exit(1)
}
if (found_account == null) {
throw AccountNotFoundException()
}
logger.debug("accounts.size: {}", accounts.size)
logger.debug("account: {}", account.anonymize())
return account
logger.debug("account: {}", found_account.anonymize())
return found_account
}
private fun cmd_stats() {
private fun cmd_stats(file_base: String, db: Database) {
println()
println("Stats:")
val format = "%40s: %d%n"
System.out.format(format, "Number of accounts", Utils.getAccounts().size)
System.out.format(format, "Number of messages", Database.getInstance().getMessageCount())
System.out.format(format, "Number of chats", Database.getInstance().getChatCount())
System.out.format(format, "Number of users", Database.getInstance().getUserCount())
System.out.format(format, "Top message ID", Database.getInstance().getTopMessageID())
System.out.format(format, "Number of accounts", Utils.getAccounts(file_base).size)
System.out.format(format, "Number of messages", db.getMessageCount())
System.out.format(format, "Number of chats", db.getChatCount())
System.out.format(format, "Number of users", db.getUserCount())
System.out.format(format, "Top message ID", db.getTopMessageID())
println()
println("Media Types:")
for ((key, value) in Database.getInstance().getMessageMediaTypesWithCount()) {
for ((key, value) in db.getMessageMediaTypesWithCount()) {
System.out.format(format, key, value)
}
println()
println("Api layers of messages:")
for ((key, value) in Database.getInstance().getMessageApiLayerWithCount()) {
for ((key, value) in db.getMessageApiLayerWithCount()) {
System.out.format(format, key, value)
}
println()
println("Message source types:")
for ((key, value) in Database.getInstance().getMessageSourceTypeWithCount()) {
for ((key, value) in db.getMessageSourceTypeWithCount()) {
System.out.format(format, key, value)
}
}
@Throws(RpcErrorException::class, IOException::class)
private fun cmd_login(phoneToUse: String?) {
val user = UserManager.getInstance()
val phone: String
if (phoneToUse == null) {
println("Please enter your phone number in international format.")
println("Example: +4917077651234")
phone = getLine()
} else {
phone = phoneToUse
}
user.sendCodeToPhoneNumber(phone)
println("Telegram sent you a code. Please enter it here.")
val code = getLine()
user.verifyCode(code)
if (user.isPasswordNeeded) {
println("We also need your account password. Please enter it now. It should not be printed, so it's okay if you see nothing while typing it.")
val pw = getPassword()
user.verifyPassword(pw)
}
storage.setPrefix("+" + user.user!!.getPhone())
System.out.println("Everything seems fine. Please run this tool again with '--account +" + user.user!!.getPhone().anonymize() + " to use this account.")
private fun cmd_login(app: TelegramApp, target_dir: String, phoneToUse: String?): Nothing {
LoginManager(app, target_dir, phoneToUse).run()
System.exit(0)
throw RuntimeException("Code never reaches this. This exists just to keep the Kotlin compiler happy.")
}
private fun show_help() {
println("Valid options are:")
println(" -h, --help Shows this help.")
println(" -a, --account <x> Use account <x>.")
println(" -l, --login Login to an existing telegram account.")
println(" --help Shows this help.")
println(" --account <x> Use account <x>.")
println(" --login Login to an existing telegram account.")
println(" --debug Shows some debug information.")
println(" --trace Shows lots of debug information. Overrides --debug.")
println(" --trace-telegram Shows lots of debug messages from the library used to access Telegram.")
println(" -A, --list-accounts List all existing accounts ")
println(" --list-accounts List all existing accounts ")
println(" --limit-messages <x> Downloads at most the most recent <x> messages.")
println(" --no-media Do not download media files.")
println(" -t, --target <x> Target directory for the files.")
println(" -e, --export <format> Export the database. Valid formats are:")
println(" --target <x> Target directory for the files.")
println(" --export <format> Export the database. Valid formats are:")
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(" csv - Creates daily CSV files for the last 7 days. Set max_file_age to change the number of days.")
println(" --license Displays the license of this program.")
println(" --daemon Keep running after the backup and automatically save new messages.")
println(" --anonymize (Try to) Remove all sensitive information from output. Useful for requesting support.")
println(" --stats Print some usage statistics.")
println(" --with-channels Backup channels as well.")
println(" --with-supergroups Backup supergroups as well.")
}
private fun list_accounts() {
println("List of available accounts:")
val accounts = Utils.getAccounts()
if (accounts.size > 0) {
for (str in accounts) {
System.out.println(" " + str.anonymize())
}
println("Use '--account <x>' to use one of those accounts.")
} else {
println("NO ACCOUNTS FOUND")
println("Use '--login' to login to a telegram account.")
}
println(" --list-channels Lists all channels together with their ID")
}
companion object {
private val logger = LoggerFactory.getLogger(CommandLineController::class.java)
public fun show_error(error: String) {
public fun show_error(error: String): Nothing {
logger.error(error)
println("ERROR: " + error)
System.exit(1)
throw RuntimeException("Code never reaches this. This exists just to keep the Kotlin compiler happy.")
}
fun show_license() {
println("TODO: Print the GPL.")
}
fun build_file_base(target_dir: String, account_to_use: String) = target_dir + File.separatorChar + account_to_use + File.separatorChar
}
class AccountNotFoundException() : Exception("Account not found") {}
class NoAccountsException() : Exception("No accounts found") {}
}

View File

@ -23,6 +23,8 @@ import de.fabianonline.telegram_backup.Utils
internal class CommandLineDownloadProgress : DownloadProgressInterface {
private var mediaCount = 0
private var i = 0
private var step = 0
private val chars = arrayOf("|", "/", "-", "\\")
override fun onMessageDownloadStart(count: Int, source: String?) {
i = 0
@ -51,13 +53,29 @@ internal class CommandLineDownloadProgress : DownloadProgressInterface {
println("'S' - Sticker 'A' - Audio 'G' - Geolocation")
println("'.' - Previously downloaded file 'e' - Empty file")
println("' ' - Ignored media type (weblinks or contacts, for example)")
println("'x' - File skipped because of errors - will be tried again at next run")
println("'x' - File skipped (because of max_file_age or max_file_size)")
println("'!' - Download failed. Will be tried again at next run.")
println("" + count + " Files to check / download")
}
override fun onMediaDownloaded(file_manager: AbstractMediaFileManager) {
show(file_manager.letter.toUpperCase())
}
override fun onMediaFileDownloadStarted() {
step = 0
print(chars[step % chars.size])
}
override fun onMediaFileDownloadStep() {
step++
print("\b")
print(chars[step % chars.size])
}
override fun onMediaFileDownloadFinished() {
print("\b")
}
override fun onMediaDownloadedEmpty() {
show("e")
@ -75,6 +93,8 @@ internal class CommandLineDownloadProgress : DownloadProgressInterface {
showNewLine()
println("Done.")
}
override fun onMediaFailed() = show("!")
private fun show(letter: String) {
print(letter)

View File

@ -15,91 +15,51 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>. */
package de.fabianonline.telegram_backup
internal object CommandLineOptions {
public var cmd_console = false
public var cmd_help = false
public var cmd_login = false
var cmd_debug = false
var cmd_trace = false
var cmd_trace_telegram = false
var cmd_list_accounts = false
var cmd_version = false
var cmd_license = false
var cmd_daemon = false
var cmd_no_media = false
var cmd_anonymize = false
var cmd_stats = false
var cmd_channels = false
var cmd_supergroups = false
var cmd_no_pagination = false
var val_account: String? = null
var val_limit_messages: Int? = null
var val_target: String? = null
var val_export: String? = null
var val_test: Int? = null
var val_pagination: Int = Config.DEFAULT_PAGINATION
@JvmStatic
fun parseOptions(args: Array<String>) {
var last_cmd: String? = null
loop@ for (arg in args) {
if (last_cmd != null) {
when (last_cmd) {
"--account" -> val_account = arg
"--limit-messages" -> val_limit_messages = Integer.parseInt(arg)
"--target" -> val_target = arg
"--export" -> val_export = arg
"--test" -> val_test = Integer.parseInt(arg)
"--pagination" -> val_pagination = Integer.parseInt(arg)
}
last_cmd = null
continue
class CommandLineOptions(args: Array<String>) {
private val values = mutableMapOf<String, String>()
var last_key: String? = null
val substitutions = mapOf("-t" to "--target")
init {
val list = args.toMutableList()
while (list.isNotEmpty()) {
var current_arg = list.removeAt(0)
if (!current_arg.startsWith("-")) throw RuntimeException("Unexpected unnamed parameter ${current_arg}")
var next_arg: String? = null
if (current_arg.contains("=")) {
val parts = current_arg.split("=", limit=2)
current_arg = parts[0]
next_arg = parts[1]
} else if (list.isNotEmpty() && !list[0].startsWith("--")) {
next_arg = list.removeAt(0)
}
when (arg) {
"-a", "--account" -> {
last_cmd = "--account"
continue@loop
}
"-h", "--help" -> cmd_help = true
"-l", "--login" -> cmd_login = true
"--debug" -> cmd_debug = true
"--trace" -> cmd_trace = true
"--trace-telegram" -> cmd_trace_telegram = true
"-A", "--list-accounts" -> cmd_list_accounts = true
"--limit-messages" -> {
last_cmd = arg
continue@loop
}
"--console" -> cmd_console = true
"-t", "--target" -> {
last_cmd = "--target"
continue@loop
}
"-V", "--version" -> cmd_version = true
"-e", "--export" -> {
last_cmd = "--export"
continue@loop
}
"--pagination" -> {
last_cmd = "--pagination"
continue@loop
}
"--no-pagination" -> cmd_no_pagination = true
"--license" -> cmd_license = true
"-d", "--daemon" -> cmd_daemon = true
"--no-media" -> cmd_no_media = true
"--test" -> {
last_cmd = "--test"
continue@loop
}
"--anonymize" -> cmd_anonymize = true
"--stats" -> cmd_stats = true
"--with-channels" -> cmd_channels = true
"--with-supergroups" -> cmd_supergroups = true
else -> throw RuntimeException("Unknown command " + arg)
if (!current_arg.startsWith("--") && current_arg.startsWith("-")) {
val replacement = substitutions.get(current_arg)
if (replacement == null) throw RuntimeException("Unknown short parameter ${current_arg}")
current_arg = replacement
}
current_arg = current_arg.substring(2)
if (next_arg == null) {
// current_arg seems to be a boolean value
values.put(current_arg, "true")
if (current_arg.startsWith("no-")) {
current_arg = current_arg.substring(3)
values.put(current_arg, "false")
}
} else {
// current_arg has the value next_arg
values.put(current_arg, next_arg)
}
}
if (last_cmd != null) {
CommandLineController.show_error("Command $last_cmd had no parameter set.")
}
}
operator fun get(name: String): String? = values[name]
fun isSet(name: String): Boolean = values[name]=="true"
}

View File

@ -19,6 +19,7 @@ package de.fabianonline.telegram_backup
import de.fabianonline.telegram_backup.CommandLineController
import de.fabianonline.telegram_backup.Utils
import de.fabianonline.telegram_backup.Version
import java.util.concurrent.TimeUnit
import org.slf4j.LoggerFactory
import ch.qos.logback.classic.Logger
import ch.qos.logback.classic.LoggerContext
@ -28,24 +29,35 @@ import ch.qos.logback.core.ConsoleAppender
import ch.qos.logback.classic.Level
fun main(args: Array<String>) {
CommandLineOptions.parseOptions(args)
CommandLineRunner.setupLogging()
CommandLineRunner.checkVersion()
if (true || CommandLineOptions.cmd_console) {
// Always use the console for now.
CommandLineController()
} else {
GUIController()
}
val clr = CommandLineRunner(args)
clr.setupLogging()
clr.checkVersion()
clr.run()
}
object CommandLineRunner {
class CommandLineRunner(args: Array<String>) {
val logger = LoggerFactory.getLogger(CommandLineRunner::class.java) as Logger
val options = CommandLineOptions(args)
fun run() {
// Always use the console for now.
try {
CommandLineController(options)
} catch (e: Throwable) {
println("An error occured!")
e.printStackTrace()
logger.error("Exception caught!", e)
System.exit(1)
}
}
fun setupLogging() {
val logger = LoggerFactory.getLogger(CommandLineRunner::class.java) as Logger
if (options.isSet("anonymize")) {
Utils.anonymize = true
}
logger.trace("Setting up Loggers...")
val rootLogger = LoggerFactory.getLogger(org.slf4j.Logger.ROOT_LOGGER_NAME) as Logger
val rootContext = rootLogger.getLoggerContext()
@ -64,30 +76,39 @@ object CommandLineRunner {
rootLogger.addAppender(appender)
rootLogger.setLevel(Level.OFF)
if (CommandLineOptions.cmd_trace) {
if (options.isSet("trace")) {
(LoggerFactory.getLogger("de.fabianonline.telegram_backup") as Logger).setLevel(Level.TRACE)
} else if (CommandLineOptions.cmd_debug) {
} else if (options.isSet("debug")) {
(LoggerFactory.getLogger("de.fabianonline.telegram_backup") as Logger).setLevel(Level.DEBUG)
}
if (CommandLineOptions.cmd_trace_telegram) {
if (options.isSet("trace_telegram")) {
(LoggerFactory.getLogger("com.github.badoualy") as Logger).setLevel(Level.TRACE)
}
}
fun checkVersion(): Boolean {
fun checkVersion() {
if (Config.APP_APPVER.contains("-")) {
println("Your version ${Config.APP_APPVER} seems to be a development version. Version check is disabled.")
return
}
val v = Utils.getNewestVersion()
if (v != null && v.isNewer) {
System.out.println("A newer version is vailable!")
System.out.println("You are using: " + Config.APP_APPVER)
System.out.println("Available: " + v.version)
System.out.println("Get it here: " + v.url)
System.out.println()
System.out.println("Changes in this version:")
System.out.println(v.body)
System.out.println()
return false
println()
println()
println()
println("A newer version is vailable!")
println("You are using: " + Config.APP_APPVER)
println("Available: " + v.version)
println("Get it here: " + v.url)
println()
println()
println("Changes in this version:")
println(v.body)
println()
println()
println()
TimeUnit.SECONDS.sleep(5)
}
return true
}
}

View File

@ -29,7 +29,7 @@ object Config {
val APP_APPVER: String
val APP_LANG = "en"
var FILE_BASE = System.getProperty("user.home") + File.separatorChar + ".telegram_backup"
var TARGET_DIR = System.getProperty("user.home") + File.separatorChar + ".telegram_backup"
val FILE_NAME_AUTH_KEY = "auth.dat"
val FILE_NAME_DC = "dc.dat"
val FILE_NAME_DB = "database.sqlite"

File diff suppressed because it is too large Load Diff

View File

@ -15,6 +15,8 @@ import org.slf4j.LoggerFactory
import org.slf4j.Logger
import de.fabianonline.telegram_backup.mediafilemanager.FileManagerFactory
import de.fabianonline.telegram_backup.mediafilemanager.AbstractMediaFileManager
import com.github.salomonbrys.kotson.*
import com.google.gson.*
class DatabaseUpdates(protected var conn: Connection, protected var db: Database) {
@ -32,34 +34,50 @@ class DatabaseUpdates(protected var conn: Connection, protected var db: Database
register(DB_Update_7(conn, db))
register(DB_Update_8(conn, db))
register(DB_Update_9(conn, db))
register(DB_Update_10(conn, db))
register(DB_Update_11(conn, db))
}
fun doUpdates() {
try {
val stmt = conn.createStatement()
var rs: ResultSet
logger.debug("DatabaseUpdate.doUpdates running")
logger.debug("Getting current database version")
val version: Int
var version: Int
logger.debug("Checking if table database_versions exists")
rs = stmt.executeQuery("SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='database_versions'")
rs.next()
if (rs.getInt(1) == 0) {
val table_count = db.queryInt("SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='database_versions'")
if (table_count == 0) {
logger.debug("Table does not exist")
version = 0
} else {
logger.debug("Table exists. Checking max version")
rs.close()
rs = stmt.executeQuery("SELECT MAX(version) FROM database_versions")
rs.next()
version = rs.getInt(1)
version = db.queryInt("SELECT MAX(version) FROM database_versions")
}
rs.close()
logger.debug("version: {}", version)
System.out.println("Database version: " + version)
logger.debug("Max available database version is {}", maxPossibleVersion)
if (version == 0) {
logger.debug("Looking for DatabaseUpdate with create_query...")
// This is a fresh database - so we search for the latest available version with a create_query
// and use this as a shortcut.
var update: DatabaseUpdate? = null
for (i in maxPossibleVersion downTo 1) {
update = getUpdateToVersion(i)
logger.trace("Looking at DatabaseUpdate version {}", update.version)
if (update.create_query != null) break
update = null
}
if (update != null) {
logger.debug("Found DatabaseUpdate version {} with create_query.", update.version)
for (query in update.create_query!!) stmt.execute(query)
stmt.execute("INSERT INTO database_versions (version) VALUES (${update.version})")
version = update.version
}
}
if (version < maxPossibleVersion) {
logger.debug("Update is necessary. {} => {}.", version, maxPossibleVersion)
var backup = false
@ -86,6 +104,9 @@ class DatabaseUpdates(protected var conn: Connection, protected var db: Database
} catch (e: SQLException) {
throw RuntimeException(e)
}
println("Cleaning up the database (this might take some time)...")
try { stmt.executeUpdate("VACUUM") } catch (t: Throwable) { logger.debug("Exception during VACUUMing: {}", t) }
} else {
logger.debug("No update necessary.")
@ -151,6 +172,8 @@ internal abstract class DatabaseUpdate(protected var conn: Connection, protected
companion object {
protected val logger = LoggerFactory.getLogger(DatabaseUpdate::class.java)
}
open val create_query: List<String>? = null
}
internal class DB_Update_1(conn: Connection, db: Database) : DatabaseUpdate(conn, db) {
@ -293,7 +316,7 @@ internal class DB_Update_6(conn: Connection, db: Database) : DatabaseUpdate(conn
} else {
ps.setInt(1, msg.getFwdFrom().getFromId())
}
val f = FileManagerFactory.getFileManager(msg, db.user_manager, db.client)
val f = FileManagerFactory.getFileManager(msg, db.file_base, settings = null)
if (f == null) {
ps.setNull(2, Types.VARCHAR)
ps.setNull(3, Types.VARCHAR)
@ -308,6 +331,7 @@ internal class DB_Update_6(conn: Connection, db: Database) : DatabaseUpdate(conn
rs.close()
conn.setAutoCommit(false)
ps.executeBatch()
ps.close()
conn.commit()
conn.setAutoCommit(true)
stmt.executeUpdate("DROP TABLE messages")
@ -376,19 +400,29 @@ internal class DB_Update_9(conn: Connection, db: Database) : DatabaseUpdate(conn
override val version: Int
get() = 9
override val needsBackup = true
override val create_query = listOf(
"CREATE TABLE \"chats\" (id INTEGER PRIMARY KEY ASC, name TEXT, type TEXT);",
"CREATE TABLE \"users\" (id INTEGER PRIMARY KEY ASC, first_name TEXT, last_name TEXT, username TEXT, type TEXT, phone TEXT);",
"CREATE TABLE database_versions (version INTEGER);",
"CREATE TABLE runs (id INTEGER PRIMARY KEY ASC, time INTEGER, start_id INTEGER, end_id INTEGER, count_missing INTEGER);",
"CREATE TABLE \"messages\" (id INTEGER PRIMARY KEY AUTOINCREMENT,message_id INTEGER,message_type TEXT,source_type TEXT,source_id INTEGER,sender_id INTEGER,fwd_from_id INTEGER,text TEXT,time INTEGER,has_media BOOLEAN,media_type TEXT,media_file TEXT,media_size INTEGER,media_json TEXT,markup_json TEXT,data BLOB,api_layer INTEGER);",
"CREATE UNIQUE INDEX unique_messages ON messages (source_type, source_id, message_id);"
)
@Throws(SQLException::class)
override fun _doUpdate() {
val logger = LoggerFactory.getLogger(DB_Update_9::class.java)
println(" Updating supergroup channel message data (this might take some time)...")
print(" ")
val count = db.queryInt("SELECT COUNT(*) FROM messages WHERE source_type='channel' and sender_id IS NULL and api_layer=53")
logger.debug("Found $count candidates for conversion")
val limit = 5000
var offset = 0
var i = 0
while (offset + 1 < count) {
while (offset < count) {
logger.debug("Querying with limit $limit and offset $offset")
val rs = stmt.executeQuery("SELECT id, data, source_id FROM messages WHERE source_type='channel' and sender_id IS NULL and api_layer=53 LIMIT ${limit} OFFSET ${offset}")
val rs = stmt.executeQuery("SELECT id, data, source_id FROM messages WHERE source_type='channel' and sender_id IS NULL and api_layer=53 ORDER BY id LIMIT ${limit} OFFSET ${offset}")
val messages = TLVector<TLAbsMessage>()
val messages_to_delete = mutableListOf<Int>()
while (rs.next()) {
@ -399,13 +433,67 @@ internal class DB_Update_9(conn: Connection, db: Database) : DatabaseUpdate(conn
messages_to_delete.add(rs.getInt(1))
}
}
db.saveMessages(messages, api_layer=53, source_type=MessageSource.SUPERGROUP)
rs.close()
db.saveMessages(messages, api_layer=53, source_type=MessageSource.SUPERGROUP, settings=null)
execute("DELETE FROM messages WHERE id IN (" + messages_to_delete.joinToString() + ")")
print(".")
offset += limit
}
println()
logger.info("Converted ${i} of ${count} messages.")
println(" Cleaning up the database (this might also take some time, sorry)...")
execute("VACUUM")
}
}
internal class DB_Update_10(conn: Connection, db: Database) : DatabaseUpdate(conn, db) {
override val version: Int
get() = 10
@Throws(SQLException::class)
override fun _doUpdate() {
execute("CREATE TABLE settings (key TEXT PRIMARY KEY, value TEXT)")
}
}
internal class DB_Update_11(conn: Connection, db: Database) : DatabaseUpdate(conn, db) {
override val version = 11
val logger = LoggerFactory.getLogger(DB_Update_11::class.java)
override fun _doUpdate() {
execute("ALTER TABLE messages ADD COLUMN json TEXT NULL")
execute("ALTER TABLE chats ADD COLUMN json TEXT NULL")
execute("ALTER TABLE chats ADD COLUMN api_layer INTEGER NULL")
execute("ALTER TABLE users ADD COLUMN json TEXT NULL")
execute("ALTER TABLE users ADD COLUMN api_layer INTEGER NULL")
val limit = 5000
var offset = 0
var i: Int
val ps = conn.prepareStatement("UPDATE messages SET json=? WHERE id=?")
println(" Updating messages to add their JSON representation to the database. This might take a few moments...")
print(" ")
do {
i = 0
logger.debug("Querying with limit $limit, offset is now $offset")
val rs = db.executeQuery("SELECT id, data FROM messages WHERE json IS NULL AND api_layer=53 LIMIT $limit")
while (rs.next()) {
i++
val id = rs.getInt(1)
val msg = Database.bytesToTLMessage(rs.getBytes(2))
val json = if (msg==null) Gson().toJson(null) else msg.toJson()
ps.setString(1, json)
ps.setInt(2, id)
ps.addBatch()
}
rs.close()
conn.setAutoCommit(false)
ps.executeBatch()
ps.clearBatch()
conn.commit()
conn.setAutoCommit(true)
offset += limit
print(".")
} while (i >= limit)
println()
ps.close()
}
}

View File

@ -18,7 +18,6 @@ package de.fabianonline.telegram_backup
import de.fabianonline.telegram_backup.UserManager
import de.fabianonline.telegram_backup.Database
import de.fabianonline.telegram_backup.StickerConverter
import de.fabianonline.telegram_backup.DownloadProgressInterface
import de.fabianonline.telegram_backup.mediafilemanager.FileManagerFactory
import de.fabianonline.telegram_backup.mediafilemanager.AbstractMediaFileManager
@ -29,6 +28,7 @@ import com.github.badoualy.telegram.tl.core.TLIntVector
import com.github.badoualy.telegram.tl.core.TLObject
import com.github.badoualy.telegram.tl.api.messages.TLAbsMessages
import com.github.badoualy.telegram.tl.api.messages.TLAbsDialogs
import com.github.badoualy.telegram.tl.api.messages.TLDialogsSlice
import com.github.badoualy.telegram.tl.api.*
import com.github.badoualy.telegram.tl.api.upload.TLFile
import com.github.badoualy.telegram.tl.exception.RpcErrorException
@ -36,6 +36,7 @@ import com.github.badoualy.telegram.tl.api.request.TLRequestUploadGetFile
import org.slf4j.LoggerFactory
import org.slf4j.Logger
import com.google.gson.Gson
import com.github.salomonbrys.kotson.*
import com.github.kittinunf.fuel.Fuel
import com.github.kittinunf.result.Result
import com.github.kittinunf.result.getAs
@ -61,70 +62,31 @@ enum class MessageSource(val descr: String) {
SUPERGROUP("supergroup")
}
class DownloadManager(internal var client: TelegramClient?, p: DownloadProgressInterface) {
internal var user: UserManager? = null
internal var db: Database? = null
internal var prog: DownloadProgressInterface? = null
internal var has_seen_flood_wait_message = false
init {
this.user = UserManager.getInstance()
this.prog = p
this.db = Database.getInstance()
}
class DownloadManager(val client: TelegramClient, val prog: DownloadProgressInterface, val db: Database, val user_manager: UserManager, val settings: Settings, val file_base: String) {
@Throws(RpcErrorException::class, IOException::class)
fun downloadMessages(limit: Int?) {
var completed: Boolean
do {
completed = true
try {
_downloadMessages(limit)
} catch (e: RpcErrorException) {
if (e.getCode() == 420) { // FLOOD_WAIT
completed = false
Utils.obeyFloodWaitException(e)
} else {
throw e
}
} catch (e: TimeoutException) {
completed = false
System.out.println("")
System.out.println("Telegram took too long to respond to our request.")
System.out.println("I'm going to wait a minute and then try again.")
try {
TimeUnit.MINUTES.sleep(1)
} catch (e2: InterruptedException) {
}
System.out.println("")
}
} while (!completed)
}
@Throws(RpcErrorException::class, IOException::class, TimeoutException::class)
fun _downloadMessages(limit: Int?) {
logger.info("This is _downloadMessages with limit {}", limit)
val dialog_limit = 100
logger.info("Downloading the last {} dialogs", dialog_limit)
logger.info("This is downloadMessages with limit {}", limit)
logger.info("Downloading the last dialogs")
System.out.println("Downloading most recent dialogs... ")
var max_message_id = 0
val dialogs = client!!.messagesGetDialogs(
0,
0,
TLInputPeerEmpty(),
dialog_limit)
logger.debug("Got {} dialogs", dialogs.getDialogs().size)
var result: ChatList? = null
Utils.obeyFloodWait() {
result = getChats()
}
val chats = result!!
logger.debug("Got {} dialogs, {} supergoups, {} channels", chats.dialogs.size, chats.supergroups.size, chats.channels.size)
for (d in dialogs.getDialogs()) {
if (d.getTopMessage() > max_message_id && d.getPeer() !is TLPeerChannel) {
for (d in chats.dialogs) {
if (d.getTopMessage() > max_message_id) {
logger.trace("Updating top message id: {} => {}. Dialog type: {}", max_message_id, d.getTopMessage(), d.getPeer().javaClass)
max_message_id = d.getTopMessage()
}
}
System.out.println("Top message ID is " + max_message_id)
var max_database_id = db!!.getTopMessageID()
var max_database_id = db.getTopMessageID()
System.out.println("Top message ID in database is " + max_database_id)
if (limit != null) {
System.out.println("Limit is set to " + limit)
@ -141,7 +103,7 @@ class DownloadManager(internal var client: TelegramClient?, p: DownloadProgressI
if (max_database_id == max_message_id) {
System.out.println("No new messages to download.")
} else if (max_database_id > max_message_id) {
throw RuntimeException("max_database_id is bigger then max_message_id. This shouldn't happen. But the telegram api nonetheless does that sometimes. Just ignore this error, wait a few seconds and then try again.")
throw RuntimeException("max_database_id is bigger than max_message_id. This shouldn't happen. But the telegram api nonetheless does that sometimes. Just ignore this error, wait a few seconds and then try again.")
} else {
val start_id = max_database_id + 1
val end_id = max_message_id
@ -152,88 +114,56 @@ class DownloadManager(internal var client: TelegramClient?, p: DownloadProgressI
logger.info("Searching for missing messages in the db")
System.out.println("Checking message database for completeness...")
val db_count = db!!.getMessageCount()
val db_max = db!!.getTopMessageID()
val db_count = db.getMessageCount()
val db_max = db.getTopMessageID()
logger.debug("db_count: {}", db_count)
logger.debug("db_max: {}", db_max)
/*if (db_count != db_max) {
if (limit != null) {
System.out.println("You are missing messages in your database. But since you're using '--limit-messages', I won't download these now.");
} else {
LinkedList<Integer> all_missing_ids = db.getMissingIDs();
LinkedList<Integer> downloadable_missing_ids = new LinkedList<Integer>();
for (Integer id : all_missing_ids) {
if (id > max_message_id - 1000000) downloadable_missing_ids.add(id);
/*if (db_count != db_max) {
if (limit != null) {
System.out.println("You are missing messages in your database. But since you're using '--limit-messages', I won't download these now.");
} else {
LinkedList<Integer> all_missing_ids = db.getMissingIDs();
LinkedList<Integer> downloadable_missing_ids = new LinkedList<Integer>();
for (Integer id : all_missing_ids) {
if (id > max_message_id - 1000000) downloadable_missing_ids.add(id);
}
count_missing = all_missing_ids.size();
System.out.println("" + all_missing_ids.size() + " messages are missing in your Database.");
System.out.println("I can (and will) download " + downloadable_missing_ids.size() + " of them.");
downloadMessages(downloadable_missing_ids, null);
}
count_missing = all_missing_ids.size();
System.out.println("" + all_missing_ids.size() + " messages are missing in your Database.");
System.out.println("I can (and will) download " + downloadable_missing_ids.size() + " of them.");
downloadMessages(downloadable_missing_ids, null);
logger.info("Logging this run");
db.logRun(Math.min(max_database_id + 1, max_message_id), max_message_id, count_missing);
}
*/
logger.info("Logging this run");
db.logRun(Math.min(max_database_id + 1, max_message_id), max_message_id, count_missing);
if (settings.download_channels) {
println("Checking channels...")
for (channel in chats.channels) { if (channel.download) downloadMessagesFromChannel(channel, limit) }
}
*/
if (settings.download_supergroups) {
println("Checking supergroups...")
for (supergroup in chats.supergroups) { if (supergroup.download) downloadMessagesFromChannel(supergroup, limit) }
}
}
if (CommandLineOptions.cmd_channels || CommandLineOptions.cmd_supergroups) {
System.out.println("Processing channels and/or supergroups...")
System.out.println("Please note that only channels/supergroups in the last 100 active chats are processed.")
val channel_access_hashes = HashMap<Int, Long>()
val channel_names = HashMap<Int, String>()
val channels = LinkedList<Int>()
val supergroups = LinkedList<Int>()
// TODO Add chat title (and other stuff?) to the database
for (c in dialogs.getChats()) {
if (c is TLChannel) {
channel_access_hashes.put(c.getId(), c.getAccessHash())
channel_names.put(c.getId(), c.getTitle())
if (c.getMegagroup()) {
supergroups.add(c.getId())
} else {
channels.add(c.getId())
}
// Channel: TLChannel
// Supergroup: getMegagroup()==true
}
private fun downloadMessagesFromChannel(channel: Channel, limit: Int?) {
val obj = channel.obj
var max_known_id = db.getTopMessageIDForChannel(channel.id)
if (obj.getTopMessage() > max_known_id) {
if (limit != null) {
max_known_id = Math.max(max_known_id, obj.getTopMessage() - limit)
}
val ids = makeIdList(max_known_id + 1, obj.getTopMessage())
var channel_name = channel.title
for (d in dialogs.getDialogs()) {
if (d.getPeer() is TLPeerChannel) {
val channel_id = (d.getPeer() as TLPeerChannel).getChannelId()
// If this is a channel and we don't want to download channels OR
// it is a supergroups and we don't want to download supergroups, then
if (channels.contains(channel_id) && !CommandLineOptions.cmd_channels || supergroups.contains(channel_id) && !CommandLineOptions.cmd_supergroups) {
// Skip this chat.
continue
}
val max_known_id = db!!.getTopMessageIDForChannel(channel_id)
if (d.getTopMessage() > max_known_id) {
val ids = makeIdList(max_known_id + 1, d.getTopMessage())
val access_hash = channel_access_hashes.get(channel_id) ?: throw RuntimeException("AccessHash for Channel missing.")
var channel_name = channel_names.get(channel_id)
if (channel_name == null) {
channel_name = "?"
}
val channel = TLInputChannel(channel_id, access_hash)
val source_type = if (supergroups.contains(channel_id)) {
MessageSource.SUPERGROUP
} else if (channels.contains(channel_id)) {
MessageSource.CHANNEL
} else {
throw RuntimeException("chat is neither in channels nor in supergroups...")
}
downloadMessages(ids, channel, source_type=source_type, source_name=channel_name)
}
}
}
val input_channel = TLInputChannel(channel.id, channel.access_hash)
val source_type = channel.message_source
downloadMessages(ids, input_channel, source_type=source_type, source_name=channel_name)
}
}
@ -246,7 +176,7 @@ class DownloadManager(internal var client: TelegramClient?, p: DownloadProgressI
} else {
"${source_type.descr} $source_name"
}
prog!!.onMessageDownloadStart(ids.size, source_string)
prog.onMessageDownloadStart(ids.size, source_string)
logger.debug("Entering download loop")
while (ids.size > 0) {
@ -261,123 +191,98 @@ class DownloadManager(internal var client: TelegramClient?, p: DownloadProgressI
logger.trace("vector.size(): {}", vector.size)
logger.trace("ids.size(): {}", ids.size)
var response: TLAbsMessages
var tries = 0
while (true) {
logger.trace("Trying getMessages(), tries={}", tries)
if (tries >= 5) {
CommandLineController.show_error("Couldn't getMessages after 5 tries. Quitting.")
}
tries++
try {
var resp: TLAbsMessages? = null
try {
Utils.obeyFloodWait(max_tries=5) {
if (channel == null) {
response = client!!.messagesGetMessages(vector)
resp = client.messagesGetMessages(vector)
} else {
response = client!!.channelsGetMessages(channel, vector)
}
break
} catch (e: RpcErrorException) {
if (e.getCode() == 420) { // FLOOD_WAIT
Utils.obeyFloodWaitException(e, has_seen_flood_wait_message)
has_seen_flood_wait_message = true
} else {
throw e
resp = client.channelsGetMessages(channel, vector)
}
}
} catch (e: MaxTriesExceededException) {
CommandLineController.show_error("Couldn't getMessages after 5 tries. Quitting.")
}
val response = resp!!
logger.trace("response.getMessages().size(): {}", response.getMessages().size)
if (response.getMessages().size != vector.size) {
CommandLineController.show_error("Requested ${vector.size} messages, but got ${response.getMessages().size}. That is unexpected. Quitting.")
}
prog!!.onMessageDownloaded(response.getMessages().size)
db!!.saveMessages(response.getMessages(), Kotlogram.API_LAYER, source_type=source_type)
db!!.saveChats(response.getChats())
db!!.saveUsers(response.getUsers())
prog.onMessageDownloaded(response.getMessages().size)
db.saveMessages(response.getMessages(), Kotlogram.API_LAYER, source_type=source_type, settings=settings)
db.saveChats(response.getChats())
db.saveUsers(response.getUsers())
logger.trace("Sleeping")
try {
TimeUnit.MILLISECONDS.sleep(Config.DELAY_AFTER_GET_MESSAGES)
} catch (e: InterruptedException) {
}
try { TimeUnit.MILLISECONDS.sleep(Config.DELAY_AFTER_GET_MESSAGES) } catch (e: InterruptedException) { }
}
logger.debug("Finished.")
prog!!.onMessageDownloadFinished()
prog.onMessageDownloadFinished()
}
@Throws(RpcErrorException::class, IOException::class)
fun downloadMedia() {
download_client = client!!.getDownloaderClient()
var completed: Boolean
do {
completed = true
try {
_downloadMedia()
} catch (e: RpcErrorException) {
if (e.getCode() == 420) { // FLOOD_WAIT
completed = false
Utils.obeyFloodWaitException(e)
} else {
throw e
}
}
/*catch (TimeoutException e) {
completed = false;
System.out.println("");
System.out.println("Telegram took too long to respond to our request.");
System.out.println("I'm going to wait a minute and then try again.");
logger.warn("TimeoutException caught", e);
try { TimeUnit.MINUTES.sleep(1); } catch(InterruptedException e2) {}
System.out.println("");
}*/
} while (!completed)
}
@Throws(RpcErrorException::class, IOException::class)
private fun _downloadMedia() {
download_client = client.getDownloaderClient()
logger.info("This is _downloadMedia")
logger.info("Checking if there are messages in the DB with a too old API layer")
val ids = db!!.getIdsFromQuery("SELECT id FROM messages WHERE has_media=1 AND api_layer<" + Kotlogram.API_LAYER)
val ids = db.getIdsFromQuery("SELECT id FROM messages WHERE has_media=1 AND api_layer<" + Kotlogram.API_LAYER)
if (ids.size > 0) {
System.out.println("You have ${ids.size} messages in your db that need an update. Doing that now.")
logger.debug("Found {} messages", ids.size)
downloadMessages(ids, null, source_type=MessageSource.NORMAL)
}
val messages = this.db!!.getMessagesWithMedia()
logger.debug("Database returned {} messages with media", messages.size)
prog!!.onMediaDownloadStart(messages.size)
for (msg in messages) {
if (msg == null) continue
val m = FileManagerFactory.getFileManager(msg, user!!, client!!)
logger.trace("message {}, {}, {}, {}, {}",
msg.getId(),
msg.getMedia().javaClass.getSimpleName().replace("TLMessageMedia", ""),
m!!.javaClass.getSimpleName(),
if (m.isEmpty) "empty" else "non-empty",
if (m.downloaded) "downloaded" else "not downloaded")
if (m.isEmpty) {
prog!!.onMediaDownloadedEmpty()
} else if (m.downloaded) {
prog!!.onMediaAlreadyPresent(m)
} else {
val message_count = db.getMessagesWithMediaCount()
prog.onMediaDownloadStart(message_count)
var offset = 0
val limit = 1000
while (true) {
logger.debug("Querying messages with media, limit={}, offset={}", limit, offset)
val messages = db.getMessagesWithMedia(limit, offset)
if (messages.size == 0) break
offset += limit
logger.debug("Database returned {} messages with media", messages.size)
for (pair in messages) {
val id = pair.first
val json = pair.second
try {
val result = m.download()
if (result) {
prog!!.onMediaDownloaded(m)
} else {
prog!!.onMediaSkipped()
val m = FileManagerFactory.getFileManager(json, file_base, settings=settings)!!
logger.trace("message {}, {}, {}, {}, {}",
id,
m.javaClass.getSimpleName(),
if (m.isEmpty) "empty" else "non-empty",
if (m.downloaded) "downloaded" else "not downloaded")
if (m.isEmpty) {
prog.onMediaDownloadedEmpty()
} else if (m.downloaded) {
prog.onMediaAlreadyPresent(m)
} else if (settings.max_file_age>0 && (System.currentTimeMillis() / 1000) - json["date"].int > settings.max_file_age * 24 * 60 * 60) {
prog.onMediaSkipped()
} else if (settings.max_file_size>0 && settings.max_file_size*1024*1024 > m.size) {
prog.onMediaSkipped()
} else if (settings.blacklist_extensions.contains(m.extension)) {
prog.onMediaSkipped()
} else {
try {
val result = m.download(prog)
if (result) {
prog.onMediaDownloaded(m)
} else {
prog.onMediaFailed()
}
} catch (e: TimeoutException) {
// do nothing - skip this file
prog.onMediaFailed()
}
} catch (e: TimeoutException) {
// do nothing - skip this file
prog!!.onMediaSkipped()
}
} catch (e: IllegalStateException) {
println(json.toPrettyJson())
throw e
}
}
}
prog!!.onMediaDownloadFinished()
prog.onMediaDownloadFinished()
}
private fun makeIdList(start: Int, end: Int): MutableList<Int> {
@ -385,6 +290,72 @@ class DownloadManager(internal var client: TelegramClient?, p: DownloadProgressI
for (i in start..end) a.add(i)
return a
}
fun getChats(): ChatList {
val cl = ChatList()
logger.debug("Getting list of chats...")
val limit = 100
var offset = 0
while (true) {
var temp: TLAbsDialogs? = null
logger.trace("Calling messagesGetDialogs with offset {}", offset)
Utils.obeyFloodWait {
temp = client.messagesGetDialogs(offset, 0, TLInputPeerEmpty(), limit)
}
val dialogs = temp!!
val last_message = dialogs.messages.filter{ it is TLMessage || it is TLMessageService }.last()
offset = when(last_message) {
is TLMessage -> last_message.date
is TLMessageService -> last_message.date
else -> throw RuntimeException("Unexpected last_message type ${last_message.javaClass}")
}
logger.trace("Got {} dialogs back", dialogs.dialogs.size)
logger.trace("New offset will be {}", offset)
// Add dialogs
cl.dialogs.addAll(dialogs.getDialogs().filter{it.getPeer() !is TLPeerChannel})
// Add supergoups and channels
for (tl_channel in dialogs.getChats().filter{it is TLChannel}.map{it as TLChannel}) {
val tl_peer_channel = dialogs.getDialogs().find{var p = it.getPeer() ; p is TLPeerChannel && p.getChannelId()==tl_channel.getId()}
if (tl_peer_channel == null) continue
var download = true
if (settings.whitelist_channels.isNotEmpty()) {
download = settings.whitelist_channels.contains(tl_channel.getId().toString())
} else if (settings.blacklist_channels.isNotEmpty()) {
download = !settings.blacklist_channels.contains(tl_channel.getId().toString())
}
val channel = Channel(id=tl_channel.getId(), access_hash=tl_channel.getAccessHash(), title=tl_channel.getTitle(), obj=tl_peer_channel, download=download)
if (tl_channel.getMegagroup()) {
channel.message_source = MessageSource.SUPERGROUP
cl.supergroups.add(channel)
} else {
channel.message_source = MessageSource.CHANNEL
cl.channels.add(channel)
}
}
if (dialogs.dialogs.size < limit) {
logger.debug("Got only ${dialogs.dialogs.size} back instead of ${limit}. Stopping the loop.")
logger.debug("Got ${cl.dialogs.size} groups, ${cl.channels.size} channels and ${cl.supergroups.size} supergroups.")
break;
}
}
return cl
}
class ChatList {
val dialogs = mutableListOf<TLDialog>()
val supergroups = mutableListOf<Channel>()
val channels = mutableListOf<Channel>()
}
class Channel(val id: Int, val access_hash: Long, val title: String, val obj: TLDialog, val download: Boolean) {
lateinit var message_source: MessageSource
}
companion object {
internal var download_client: TelegramClient? = null
@ -392,19 +363,19 @@ class DownloadManager(internal var client: TelegramClient?, p: DownloadProgressI
internal val logger = LoggerFactory.getLogger(DownloadManager::class.java)
@Throws(RpcErrorException::class, IOException::class, TimeoutException::class)
fun downloadFile(targetFilename: String, size: Int, dcId: Int, volumeId: Long, localId: Int, secret: Long) {
fun downloadFile(targetFilename: String, size: Int, dcId: Int, volumeId: Long, localId: Int, secret: Long, prog: DownloadProgressInterface?) {
val loc = TLInputFileLocation(volumeId, localId, secret)
downloadFileFromDc(targetFilename, loc, dcId, size)
downloadFileFromDc(targetFilename, loc, dcId, size, prog)
}
@Throws(RpcErrorException::class, IOException::class, TimeoutException::class)
fun downloadFile(targetFilename: String, size: Int, dcId: Int, id: Long, accessHash: Long) {
fun downloadFile(targetFilename: String, size: Int, dcId: Int, id: Long, accessHash: Long, prog: DownloadProgressInterface?) {
val loc = TLInputDocumentFileLocation(id, accessHash)
downloadFileFromDc(targetFilename, loc, dcId, size)
downloadFileFromDc(targetFilename, loc, dcId, size, prog)
}
@Throws(RpcErrorException::class, IOException::class, TimeoutException::class)
private fun downloadFileFromDc(target: String, loc: TLAbsInputFileLocation, dcID: Int, size: Int): Boolean {
private fun downloadFileFromDc(target: String, loc: TLAbsInputFileLocation, dcID: Int, size: Int, prog: DownloadProgressInterface?): Boolean {
var fos: FileOutputStream? = null
try {
val temp_filename = target + ".downloading"
@ -423,38 +394,34 @@ class DownloadManager(internal var client: TelegramClient?, p: DownloadProgressI
}
logger.trace("offset before the loop is {}", offset)
fos = FileOutputStream(temp_filename, true)
var response: TLFile? = null
var try_again: Boolean
if (prog != null) prog.onMediaFileDownloadStarted()
do {
try_again = false
logger.trace("offset: {} block_size: {} size: {}", offset, size, size)
val req = TLRequestUploadGetFile(loc, offset, size)
var resp: TLFile? = null
try {
response = download_client!!.executeRpcQuery(req, dcID) as TLFile
} catch (e: RpcErrorException) {
if (e.getCode() == 420) { // FLOOD_WAIT
try_again = true
Utils.obeyFloodWaitException(e)
continue // response is null since we didn't actually receive any data. Skip the rest of this iteration and try again.
} else if (e.getCode() == 400) {
//Somehow this file is broken. No idea why. Let's skip it for now
return false
} else {
throw e
Utils.obeyFloodWait() {
resp = download_client!!.executeRpcQuery(req, dcID) as TLFile
}
} catch (e: RpcErrorException) {
if (e.getCode() == 400) {
// Somehow this file is broken. No idea why. Let's skip it for now.
return false
}
throw e
}
val response = resp!!
if (prog!=null) prog.onMediaFileDownloadStep()
offset += response.getBytes().getData().size
logger.trace("response: {} total size: {}", response.getBytes().getData().size, offset)
fos.write(response.getBytes().getData())
fos.flush()
try {
TimeUnit.MILLISECONDS.sleep(Config.DELAY_AFTER_GET_FILE)
} catch (e: InterruptedException) {
}
try { TimeUnit.MILLISECONDS.sleep(Config.DELAY_AFTER_GET_FILE) } catch (e: InterruptedException) { }
} while (offset < size && (try_again || response!!.getBytes().getData().size > 0))
} while (offset < size && response.getBytes().getData().size > 0)
if (prog != null) prog.onMediaFileDownloadFinished()
fos.close()
if (offset < size) {
System.out.println("Requested file $target with $size bytes, but got only $offset bytes.")
@ -509,13 +476,18 @@ class DownloadManager(internal var client: TelegramClient?, p: DownloadProgressI
}
@Throws(IOException::class)
fun downloadExternalFile(target: String, url: String): Boolean {
val (_, response, result) = Fuel.get(url).response()
if (result is Result.Success) {
File(target).writeBytes(response.data)
return true
fun downloadExternalFile(target: String, url: String, prog: DownloadProgressInterface?): Boolean {
if (prog != null) prog.onMediaFileDownloadStarted()
var success = true
Fuel.download(url).destination { _, _ ->
File(target)
}.progress { _, _ ->
if (prog != null) prog.onMediaFileDownloadStep()
}.response { _, _, result ->
success = (result is Result.Success)
}
return false
if (prog != null) prog.onMediaFileDownloadFinished()
return success
}
}
}

View File

@ -29,4 +29,8 @@ interface DownloadProgressInterface {
fun onMediaSkipped()
fun onMediaAlreadyPresent(file_manager: AbstractMediaFileManager)
fun onMediaDownloadFinished()
fun onMediaFailed()
fun onMediaFileDownloadStarted()
fun onMediaFileDownloadStep()
fun onMediaFileDownloadFinished()
}

View File

@ -1,85 +0,0 @@
/* 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 javax.swing.*
import javax.swing.event.ListSelectionEvent
import javax.swing.event.ListSelectionListener
import java.awt.*
import java.awt.event.ActionEvent
import java.awt.event.ActionListener
import java.util.Vector
class GUIController() {
init {
showAccountChooserDialog()
}
private fun showAccountChooserDialog() {
val accountChooser = JDialog()
accountChooser.setTitle("Choose account")
accountChooser.setSize(400, 200)
val vert = JPanel()
vert.setLayout(BorderLayout())
vert.add(JLabel("Please select the account to use or create a new one."), BorderLayout.NORTH)
val accounts = Utils.getAccounts()
val list = JList<String>(accounts)
list.setSelectionMode(ListSelectionModel.SINGLE_SELECTION)
vert.add(list, BorderLayout.CENTER)
val bottom = JPanel(GridLayout(1, 2))
val btnAddAccount = JButton("Add account")
bottom.add(btnAddAccount)
val btnLogin = JButton("Login")
btnLogin.setEnabled(false)
bottom.add(btnLogin)
vert.add(bottom, BorderLayout.SOUTH)
accountChooser.add(vert)
accountChooser.setVisible(true)
accountChooser.setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE)
}
private fun addAccountDialog() {
val loginDialog = JDialog()
loginDialog.setTitle("Add an account")
loginDialog.setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE)
val sections = JPanel()
sections.setLayout(BoxLayout(sections, BoxLayout.Y_AXIS))
val top = JPanel()
top.setLayout(BoxLayout(top, BoxLayout.Y_AXIS))
top.add(JLabel("Please enter your phone number in international format:"))
top.add(JTextField("+49123773212"))
sections.add(top)
sections.add(Box.createVerticalStrut(5))
sections.add(JSeparator(SwingConstants.HORIZONTAL))
val middle = JPanel()
middle.setLayout(BoxLayout(middle, BoxLayout.Y_AXIS))
middle.add(JLabel("Telegram sent you a code. Enter it here:"))
middle.add(JTextField())
middle.setEnabled(false)
sections.add(middle)
sections.add(Box.createVerticalStrut(5))
sections.add(JSeparator(SwingConstants.HORIZONTAL))
loginDialog.add(sections)
loginDialog.setVisible(true)
}
}

View File

@ -0,0 +1,96 @@
package de.fabianonline.telegram_backup
import com.github.badoualy.telegram.api.Kotlogram
import com.github.badoualy.telegram.api.TelegramApp
import com.github.badoualy.telegram.api.TelegramClient
import com.github.badoualy.telegram.tl.api.TLUser
import com.github.badoualy.telegram.tl.api.account.TLPassword
import com.github.badoualy.telegram.tl.api.auth.TLSentCode
import com.github.badoualy.telegram.tl.core.TLBytes
import com.github.badoualy.telegram.tl.exception.RpcErrorException
import java.security.MessageDigest
import java.util.*
class LoginManager(val app: TelegramApp, val target_dir: String, val phoneToUse: String?) {
fun run() {
var phone: String
if (phoneToUse == null) {
println("Please enter your phone number in international format.")
println("Example: +4917077651234")
phone = getLine()
} else {
phone = phoneToUse
}
val file_base = CommandLineController.build_file_base(target_dir, phone)
// We now have an account, so we can create an ApiStorage and TelegramClient.
val storage = ApiStorage(file_base)
val client = Kotlogram.getDefaultClient(app, storage, Kotlogram.PROD_DC4, null)
val sent_code = send_code_to_phone_number(client, phone)
println("Telegram sent you a code. Please enter it here.")
val code = getLine()
try {
verify_code(client, phone, sent_code, code)
} catch(e: PasswordNeededException) {
println("We also need your account password. Please enter it now. It should not be printed, so it's okay if you see nothing while typing it.")
val pw = getPassword()
verify_password(client, pw)
}
System.out.println("Everything seems fine. Please run this tool again with '--account ${phone} to use this account.")
}
private fun send_code_to_phone_number(client: TelegramClient, phone: String): TLSentCode {
return client.authSendCode(false, phone, true)
}
private fun verify_code(client: TelegramClient, phone: String, sent_code: TLSentCode, code: String): TLUser {
try {
val auth = client.authSignIn(phone, sent_code.getPhoneCodeHash(), code)
return auth.getUser().getAsUser()
} catch (e: RpcErrorException) {
if (e.getCode() == 401 && e.getTag()=="SESSION_PASSWORD_NEEDED") {
throw PasswordNeededException()
} else {
throw e
}
}
}
private fun verify_password(client: TelegramClient, password: String): TLUser {
val pw = password.toByteArray(charset = Charsets.UTF_8)
val salt = (client.accountGetPassword() as TLPassword).getCurrentSalt().getData()
val md = MessageDigest.getInstance("SHA-256")
val salted = ByteArray(2 * salt.size + pw.size)
System.arraycopy(salt, 0, salted, 0, salt.size)
System.arraycopy(pw, 0, salted, salt.size, pw.size)
System.arraycopy(salt, 0, salted, salt.size + pw.size, salt.size)
val hash = md.digest(salted)
val auth = client.authCheckPassword(TLBytes(hash))
return auth.getUser().getAsUser()
}
private fun getLine(): String {
if (System.console() != null) {
return System.console().readLine("> ")
} else {
print("> ")
return Scanner(System.`in`).nextLine()
}
}
private fun getPassword(): String {
if (System.console() != null) {
return String(System.console().readPassword("> "))
} else {
return getLine()
}
}
}
class PasswordNeededException: Exception("A password is needed to be able to login to this account.") {}

View File

@ -0,0 +1,137 @@
package de.fabianonline.telegram_backup
import java.io.File
import java.util.LinkedList
import org.slf4j.LoggerFactory
class Settings(val file_base: String, val database: Database, val cli_settings: CommandLineOptions) {
val logger = LoggerFactory.getLogger(Settings::class.java)
private val db_settings: Map<String, String>
val ini_settings: Map<String, List<String>>
init {
db_settings = database.fetchSettings()
ini_settings = load_ini("config.ini")
copy_sample_ini("config.sample.ini")
}
// Merging CLI and INI settings
val sf = SettingsFactory(ini_settings, cli_settings)
val gmaps_key = sf.getString("gmaps_key", default=Config.SECRET_GMAPS, secret=true)
val pagination = sf.getBoolean("pagination", default=true)
val pagination_size = sf.getInt("pagination_size", default=Config.DEFAULT_PAGINATION)
val download_media = sf.getBoolean("download_media", default=true)
val download_channels = sf.getBoolean("download_channels", default=false)
val download_supergroups = sf.getBoolean("download_supergroups", default=false)
val whitelist_channels = sf.getStringList("whitelist_channels", default=LinkedList<String>())
val blacklist_channels = sf.getStringList("blacklist_channels", default=LinkedList<String>())
val max_file_age = sf.getInt("max_file_age", default=-1)
val max_file_size = sf.getInt("max_file_size", default=-1)
val blacklist_extensions = sf.getStringList("blacklist_extensions", default=LinkedList<String>())
private fun get_setting_list(name: String): List<String>? {
return ini_settings[name]
}
private fun load_ini(filename: String): Map<String, List<String>> {
val map = mutableMapOf<String, MutableList<String>>()
val file = File(file_base + filename)
logger.trace("Checking ini file {}", filename.anonymize())
if (!file.exists()) return map
logger.debug("Loading ini file {}", filename.anonymize())
file.forEachLine { parseLine(it, map) }
return map
}
private fun parseLine(original_line: String, map: MutableMap<String, MutableList<String>>) {
logger.trace("Parsing line: {}", original_line)
var line = original_line.trim().replaceAfter("#", "").removeSuffix("#")
logger.trace("After cleaning: {}", line)
if (line == "") return
val parts: List<String> = line.split("=", limit=2).map{it.trim()}
if (parts.size < 2) throw RuntimeException("Invalid config setting: $line")
val (key, value) = parts
if (value=="") {
map.remove(key)
} else {
var list = map.get(key)
if (list == null) {
list = mutableListOf<String>()
map.put(key, list)
}
list.add(value)
}
}
private fun copy_sample_ini(filename: String) {
val stream = Config::class.java.getResourceAsStream("/config.sample.ini")
File(file_base + filename).outputStream().use { stream.copyTo(it) }
stream.close()
}
fun print() {
println()
Setting.all_settings.forEach { it.print() }
println()
}
}
class SettingsFactory(val ini: Map<String, List<String>>, val cli: CommandLineOptions) {
fun getInt(name: String, default: Int, secret: Boolean = false) = getSetting(name, listOf(default.toString()), secret).get().toInt()
fun getBoolean(name: String, default: Boolean, secret: Boolean = false) = getSetting(name, listOf(default.toString()), secret).get().toBoolean()
fun getString(name: String, default: String, secret: Boolean = false) = getSetting(name, listOf(default), secret).get()
fun getStringList(name: String, default: List<String>, secret: Boolean = false) = getSetting(name, default, secret).getList()
fun getSetting(name: String, default: List<String>, secret: Boolean) = Setting(ini, cli, name, default, secret)
}
class Setting(val ini: Map<String, List<String>>, val cli: CommandLineOptions, val name: String, val default: List<String>, val secret: Boolean) {
val values: List<String>
val source: SettingSource
val logger = LoggerFactory.getLogger(Setting::class.java)
init {
if (getCli(name) != null) {
values = listOf(getCli(name)!!)
source = SettingSource.CLI
} else if (getIni(name) != null) {
values = getIni(name)!!
source = SettingSource.INI
} else {
values = default
source = SettingSource.DEFAULT
}
logger.debug("Setting ${name} loaded. Source: ${source}. Value: ${values.toString().anonymize()}")
all_settings.add(this)
}
fun get(): String = values.last()
fun getList(): List<String> = values
fun getIni(name: String): List<String>? {
return ini[name]
}
fun getCli(name: String): String? {
return cli.get(name)
}
fun print() {
println("%-25s %-10s %s".format(name, source, (if (secret && source==SettingSource.DEFAULT) "[REDACTED]" else values)))
}
companion object {
val all_settings = LinkedList<Setting>()
}
}
enum class SettingSource {
INI,
CLI,
DEFAULT
}

View File

@ -1,52 +0,0 @@
/* 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 java.lang.StringBuilder
import java.io.File
object StickerConverter {
fun makeFilenameWithPath(attr: TLDocumentAttributeSticker): String {
val file = StringBuilder()
file.append(makePath())
file.append(makeFilename(attr))
return file.toString()
}
fun makeFilename(attr: TLDocumentAttributeSticker): String {
val file = StringBuilder()
if (attr.getStickerset() is TLInputStickerSetShortName) {
file.append((attr.getStickerset() as TLInputStickerSetShortName).getShortName())
} else if (attr.getStickerset() is TLInputStickerSetID) {
file.append((attr.getStickerset() as TLInputStickerSetID).getId())
}
file.append("_")
file.append(attr.getAlt().hashCode())
file.append(".webp")
return file.toString()
}
fun makePath(): String {
val path = Config.FILE_BASE +
File.separatorChar +
Config.FILE_STICKER_BASE +
File.separatorChar
File(path).mkdirs()
return path
}
}

View File

@ -26,48 +26,39 @@ import de.fabianonline.telegram_backup.Database
import de.fabianonline.telegram_backup.UserManager
import de.fabianonline.telegram_backup.mediafilemanager.AbstractMediaFileManager
import de.fabianonline.telegram_backup.mediafilemanager.FileManagerFactory
import org.slf4j.LoggerFactory
internal class TelegramUpdateHandler : UpdateCallback {
private var user: UserManager? = null
private var db: Database? = null
var debug = false
fun activate() {
this.user = UserManager.getInstance()
this.db = Database.getInstance()
}
internal class TelegramUpdateHandler(val user_manager: UserManager, val db: Database, val file_base: String, val settings: Settings) : UpdateCallback {
val logger = LoggerFactory.getLogger(TelegramUpdateHandler::class.java)
override fun onUpdates(client: TelegramClient, updates: TLUpdates) {
if (db == null) return
if (debug) System.out.println("onUpdates - " + updates.getUpdates().size + " Updates, " + updates.getUsers().size + " Users, " + updates.getChats().size + " Chats")
logger.debug("onUpdates - " + updates.getUpdates().size + " Updates, " + updates.getUsers().size + " Users, " + updates.getChats().size + " Chats")
for (update in updates.getUpdates()) {
processUpdate(update, client)
if (debug) System.out.println(" " + update.javaClass.getName())
processUpdate(update)
logger.debug(" " + update.javaClass.getName())
}
db!!.saveUsers(updates.getUsers())
db!!.saveChats(updates.getChats())
db.saveUsers(updates.getUsers())
db.saveChats(updates.getChats())
}
override fun onUpdatesCombined(client: TelegramClient, updates: TLUpdatesCombined) {
if (db == null) return
if (debug) System.out.println("onUpdatesCombined")
logger.debug("onUpdatesCombined")
for (update in updates.getUpdates()) {
processUpdate(update, client)
processUpdate(update)
}
db!!.saveUsers(updates.getUsers())
db!!.saveChats(updates.getChats())
db.saveUsers(updates.getUsers())
db.saveChats(updates.getChats())
}
override fun onUpdateShort(client: TelegramClient, update: TLUpdateShort) {
if (db == null) return
if (debug) System.out.println("onUpdateShort")
processUpdate(update.getUpdate(), client)
if (debug) System.out.println(" " + update.getUpdate().javaClass.getName())
logger.debug("onUpdateShort")
processUpdate(update.getUpdate())
logger.debug(" " + update.getUpdate().javaClass.getName())
}
override fun onShortChatMessage(client: TelegramClient, message: TLUpdateShortChatMessage) {
if (db == null) return
if (debug) System.out.println("onShortChatMessage - " + message.getMessage())
logger.debug("onShortChatMessage - " + message.getMessage())
val msg = TLMessage(
message.getOut(),
message.getMentioned(),
@ -85,21 +76,20 @@ internal class TelegramUpdateHandler : UpdateCallback {
message.getEntities(), null, null)
val vector = TLVector<TLAbsMessage>(TLAbsMessage::class.java)
vector.add(msg)
db!!.saveMessages(vector, Kotlogram.API_LAYER)
db.saveMessages(vector, Kotlogram.API_LAYER, settings=settings)
System.out.print('.')
}
override fun onShortMessage(client: TelegramClient, message: TLUpdateShortMessage) {
val m = message
if (db == null) return
if (debug) System.out.println("onShortMessage - " + m.getOut() + " - " + m.getUserId() + " - " + m.getMessage())
logger.debug("onShortMessage - " + m.getOut() + " - " + m.getUserId() + " - " + m.getMessage())
val from_id: Int
val to_id: Int
if (m.getOut() == true) {
from_id = user!!.user!!.getId()
from_id = user_manager.id
to_id = m.getUserId()
} else {
to_id = user!!.user!!.getId()
to_id = user_manager.id
from_id = m.getUserId()
}
val msg = TLMessage(
@ -119,29 +109,27 @@ internal class TelegramUpdateHandler : UpdateCallback {
m.getEntities(), null, null)
val vector = TLVector<TLAbsMessage>(TLAbsMessage::class.java)
vector.add(msg)
db!!.saveMessages(vector, Kotlogram.API_LAYER)
db.saveMessages(vector, Kotlogram.API_LAYER, settings=settings)
System.out.print('.')
}
override fun onShortSentMessage(client: TelegramClient, message: TLUpdateShortSentMessage) {
if (db == null) return
System.out.println("onShortSentMessage")
logger.debug("onShortSentMessage")
}
override fun onUpdateTooLong(client: TelegramClient) {
if (db == null) return
System.out.println("onUpdateTooLong")
logger.debug("onUpdateTooLong")
}
private fun processUpdate(update: TLAbsUpdate, client: TelegramClient) {
private fun processUpdate(update: TLAbsUpdate) {
if (update is TLUpdateNewMessage) {
val abs_msg = update.getMessage()
val vector = TLVector<TLAbsMessage>(TLAbsMessage::class.java)
vector.add(abs_msg)
db!!.saveMessages(vector, Kotlogram.API_LAYER)
db.saveMessages(vector, Kotlogram.API_LAYER, settings=settings)
System.out.print('.')
if (abs_msg is TLMessage) {
val fm = FileManagerFactory.getFileManager(abs_msg, user!!, client)
if (abs_msg is TLMessage && settings.download_media==true) {
val fm = FileManagerFactory.getFileManager(abs_msg, file_base, settings)
if (fm != null && !fm.isEmpty && !fm.downloaded) {
try {
fm.download()

View File

@ -10,7 +10,7 @@ import java.sql.ResultSet
import java.io.IOException
import java.nio.charset.Charset
internal object TestFeatures {
internal class TestFeatures(val db: Database) {
fun test1() {
// Tests entries in a cache4.db in the current working directory for compatibility
try {
@ -24,31 +24,22 @@ internal object TestFeatures {
var conn: Connection
var stmt: Statement? = null
try {
conn = DriverManager.getConnection(path)
stmt = conn.createStatement()
} catch (e: SQLException) {
CommandLineController.show_error("Could not connect to SQLITE database.")
}
conn = DriverManager.getConnection(path)
stmt = conn.createStatement()
var unsupported_constructor = 0
var success = 0
try {
val rs = stmt!!.executeQuery("SELECT data FROM messages")
while (rs.next()) {
try {
TLApiContext.getInstance().deserializeMessage(rs.getBytes(1))
} catch (e: com.github.badoualy.telegram.tl.exception.UnsupportedConstructorException) {
unsupported_constructor++
} catch (e: IOException) {
System.out.println("IOException: " + e)
}
success++
val rs = stmt.executeQuery("SELECT data FROM messages")
while (rs.next()) {
try {
TLApiContext.getInstance().deserializeMessage(rs.getBytes(1))
} catch (e: com.github.badoualy.telegram.tl.exception.UnsupportedConstructorException) {
unsupported_constructor++
} catch (e: IOException) {
System.out.println("IOException: " + e)
}
} catch (e: SQLException) {
System.out.println("SQL exception: " + e)
success++
}
System.out.println("Success: " + success)
@ -59,7 +50,6 @@ internal object TestFeatures {
// Prints system.encoding and default charset
System.out.println("Default Charset: " + Charset.defaultCharset())
System.out.println("file.encoding: " + System.getProperty("file.encoding"))
val db = Database.getInstance()
System.out.println("Database encoding: " + db.getEncoding())
}
}

View File

@ -34,107 +34,34 @@ import java.io.File
import org.slf4j.LoggerFactory
import org.slf4j.Logger
class UserManager @Throws(IOException::class)
private constructor(c: TelegramClient) {
var user: TLUser? = null
var phone: String? = null
private var code: String? = null
private val client: TelegramClient
private var sent_code: TLSentCode? = null
private var auth: TLAuthorization? = null
var isPasswordNeeded = false
private set
val loggedIn: Boolean
get() = user != null
val userString: String
get() {
if (this.user == null) return "Not logged in"
val sb = StringBuilder()
if (this.user!!.getFirstName() != null) {
sb.append(this.user!!.getFirstName())
}
if (this.user!!.getLastName() != null) {
sb.append(" ")
sb.append(this.user!!.getLastName())
}
if (this.user!!.getUsername() != null) {
sb.append(" (@")
sb.append(this.user!!.getUsername())
sb.append(")")
}
return sb.toString()
}
val fileBase: String
get() = Config.FILE_BASE + File.separatorChar + "+" + this.user!!.getPhone() + File.separatorChar
class UserManager(val client: TelegramClient) {
val tl_user: TLUser
val logger = LoggerFactory.getLogger(UserManager::class.java)
val phone: String
get() = "+" + tl_user.getPhone()
val id: Int
get() = tl_user.getId()
init {
this.client = c
logger.debug("Calling getFullUser")
try {
val full_user = this.client.usersGetFullUser(TLInputUserSelf())
this.user = full_user.getUser().getAsUser()
} catch (e: RpcErrorException) {
// This may happen. Ignoring it.
logger.debug("Ignoring exception:", e)
}
val full_user = client.usersGetFullUser(TLInputUserSelf())
tl_user = full_user.getUser().getAsUser()
}
@Throws(RpcErrorException::class, IOException::class)
fun sendCodeToPhoneNumber(number: String) {
this.phone = number
this.sent_code = this.client.authSendCode(false, number, true)
}
@Throws(RpcErrorException::class, IOException::class)
fun verifyCode(code: String) {
this.code = code
try {
this.auth = client.authSignIn(phone, this.sent_code!!.getPhoneCodeHash(), this.code)
this.user = auth!!.getUser().getAsUser()
} catch (e: RpcErrorException) {
if (e.getCode() != 401 || !e.getTag().equals("SESSION_PASSWORD_NEEDED")) throw e
this.isPasswordNeeded = true
override fun toString(): String {
val sb = StringBuilder()
sb.append(tl_user.getFirstName() ?: "")
if (tl_user.getLastName() != null) {
sb.append(" ")
sb.append(tl_user.getLastName())
}
}
@Throws(RpcErrorException::class, IOException::class)
fun verifyPassword(pw: String) {
val password = pw.toByteArray(charset = Charsets.UTF_8)
val salt = (client.accountGetPassword() as TLPassword).getCurrentSalt().getData()
var md: MessageDigest
try {
md = MessageDigest.getInstance("SHA-256")
} catch (e: NoSuchAlgorithmException) {
e.printStackTrace()
return
}
val salted = ByteArray(2 * salt.size + password.size)
System.arraycopy(salt, 0, salted, 0, salt.size)
System.arraycopy(password, 0, salted, salt.size, password.size)
System.arraycopy(salt, 0, salted, salt.size + password.size, salt.size)
val hash = md.digest(salted)
auth = client.authCheckPassword(TLBytes(hash))
this.user = auth!!.getUser().getAsUser()
}
companion object {
private val logger = LoggerFactory.getLogger(UserManager::class.java)
internal var instance: UserManager? = null
@Throws(IOException::class)
fun init(c: TelegramClient) {
instance = UserManager(c)
}
fun getInstance(): UserManager {
if (instance == null) throw RuntimeException("UserManager is not yet initialized.")
return instance!!
if (tl_user.getUsername() != null) {
sb.append(" (@")
sb.append(tl_user.getUsername())
sb.append(")")
}
return sb.toString()
}
}

View File

@ -17,10 +17,16 @@
package de.fabianonline.telegram_backup
import com.github.badoualy.telegram.tl.exception.RpcErrorException
import com.github.badoualy.telegram.tl.api.TLAbsMessage
import com.github.badoualy.telegram.tl.api.TLAbsUser
import com.github.badoualy.telegram.tl.api.TLAbsChat
import com.github.badoualy.telegram.api.Kotlogram
import java.io.File
import java.util.Vector
import java.util.concurrent.TimeUnit
import java.util.concurrent.TimeoutException
import com.google.gson.*
import com.github.salomonbrys.kotson.*
import java.net.URL
import org.apache.commons.io.IOUtils
import de.fabianonline.telegram_backup.Version
@ -31,12 +37,30 @@ object Utils {
@JvmField public val VERSIONS_EQUAL = 0
@JvmField public val VERSION_1_NEWER = 1
@JvmField public val VERSION_2_NEWER = 2
var hasSeenFloodWaitMessage = false
var anonymize = false
private val logger = LoggerFactory.getLogger(Utils::class.java) as Logger
fun getAccounts(): Vector<String> {
fun print_accounts(file_base: String) {
println("List of available accounts:")
val accounts = getAccounts(file_base)
if (accounts.size > 0) {
for (str in accounts) {
println(" " + str.anonymize())
}
println("Use '--account <x>' to use one of those accounts.")
} else {
println("NO ACCOUNTS FOUND")
println("Use '--login' to login to a telegram account.")
}
}
fun getAccounts(file_base: String): Vector<String> {
val accounts = Vector<String>()
val folder = File(Config.FILE_BASE)
val folder = File(file_base)
val files = folder.listFiles()
if (files != null)
for (f in files) {
@ -80,30 +104,56 @@ object Utils {
}
}
@Throws(RpcErrorException::class)
@JvmOverloads internal fun obeyFloodWaitException(e: RpcErrorException?, silent: Boolean = false) {
if (e == null || e.getCode() != 420) return
val delay: Long = e.getTagInteger()!!.toLong()
if (!silent) {
System.out.println("")
System.out.println(
"Telegram complained about us (okay, me) making too many requests in too short time by\n" +
"sending us \"" + e.getTag() + "\" as an error. So we now have to wait a bit. Telegram\n" +
"asked us to wait for " + delay + " seconds.\n" +
fun obeyFloodWait(max_tries: Int = -1, method: () -> Unit) {
var tries = 0
while (true) {
tries++
if (max_tries>0 && tries>max_tries) throw MaxTriesExceededException()
logger.trace("This is try ${tries}.")
try {
method.invoke()
// If we reach this, the method has returned successfully -> we are done
return
} catch(e: RpcErrorException) {
// If we got something else than a FLOOD_WAIT error, we just rethrow it
if (e.getCode() != 420) throw e
val delay = e.getTagInteger()!!.toLong()
if (!hasSeenFloodWaitMessage) {
println(
"\n" +
"Telegram complained about us (okay, me) making too many requests in too short time by\n" +
"sending us \"${e.getTag()}\" as an error. So we now have to wait a bit. Telegram\n" +
"asked us to wait for ${delay} seconds.\n" +
"\n" +
"So I'm going to do just that for now. If you don't want to wait, you can quit by pressing\n" +
"Ctrl+C. You can restart me at any time and I will just continue to download your\n" +
"messages and media. But be advised that just restarting me is not going to change\n" +
"the fact that Telegram won't talk to me until then." +
"\n")
} else {
print(" Waiting...")
}
try { TimeUnit.SECONDS.sleep(delay + 1) } catch (e: InterruptedException) { }
if (hasSeenFloodWaitMessage) {
// " W a i t i n g . . ."
print("\b\b\b\b\b\b\b\b\b\b\b")
}
hasSeenFloodWaitMessage = true
} catch (e: TimeoutException) {
println(
"\n" +
"So I'm going to do just that for now. If you don't want to wait, you can quit by pressing\n" +
"Ctrl+C. You can restart me at any time and I will just continue to download your\n" +
"messages and media. But be advised that just restarting me is not going to change\n" +
"the fact that Telegram won't talk to me until then.")
System.out.println("")
"Telegram took too long to respond to our request.\n" +
"I'm going to wait a minute and then try again." +
"\n")
try { TimeUnit.MINUTES.sleep(1) } catch (e: InterruptedException) { }
}
}
try {
TimeUnit.SECONDS.sleep(delay + 1)
} catch (e2: InterruptedException) {
}
}
@JvmStatic
@ -179,8 +229,48 @@ object Utils {
}
fun String.anonymize(): String {
return if (!CommandLineOptions.cmd_anonymize) this else this.replace(Regex("[0-9]"), "1").replace(Regex("[A-Z]"), "A").replace(Regex("[a-z]"), "a") + " (ANONYMIZED)"
return if (!Utils.anonymize) this else this.replace(Regex("[0-9]"), "1").replace(Regex("[A-Z]"), "A").replace(Regex("[a-z]"), "a") + " (ANONYMIZED)"
}
fun Any.toJson(): String = Gson().toJson(this)
fun Any.toPrettyJson(): String = GsonBuilder().setPrettyPrinting().create().toJson(this)
fun JsonObject.isA(name: String): Boolean = this.contains("_constructor") && this["_constructor"].string.startsWith(name + "#")
fun JsonElement.isA(name: String): Boolean = this.obj.isA(name)
class MaxTriesExceededException(): RuntimeException("Max tries exceeded") {}
fun TLAbsMessage.toJson(): String {
val json = Gson().toJsonTree(this).obj
cleanUpMessageJson(json)
json["api_layer"] = Kotlogram.API_LAYER
return json.toString()
}
fun TLAbsChat.toJson(): String {
val json = Gson().toJsonTree(this).obj
json["api_layer"] = Kotlogram.API_LAYER
return json.toString()
}
fun TLAbsUser.toJson(): String {
val json = Gson().toJsonTree(this).obj
json["api_layer"] = Kotlogram.API_LAYER
return json.toString()
}
fun cleanUpMessageJson(json : JsonElement) {
if (json.isJsonArray) {
json.array.forEach {cleanUpMessageJson(it)}
return
} else if (!json.isJsonObject) {
return
}
if (json.obj.has("bytes")) {
json.obj -= "bytes"
return
}
json.obj.forEach {_: String, elm: JsonElement ->
if (elm.isJsonObject || elm.isJsonArray) cleanUpMessageJson(elm)
}
}

View File

@ -0,0 +1,111 @@
/* 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.exporter
import java.io.File
import java.io.PrintWriter
import java.io.OutputStreamWriter
import java.io.FileOutputStream
import java.nio.charset.Charset
import java.io.FileWriter
import java.io.IOException
import java.io.FileNotFoundException
import java.net.URL
import org.apache.commons.io.FileUtils
import java.util.LinkedList
import java.util.HashMap
import java.time.LocalDate
import java.time.LocalTime
import java.time.LocalDateTime
import java.time.ZoneOffset
import java.time.format.DateTimeFormatter
import java.sql.Time
import java.text.SimpleDateFormat
import com.github.mustachejava.DefaultMustacheFactory
import com.github.mustachejava.Mustache
import com.github.mustachejava.MustacheFactory
import de.fabianonline.telegram_backup.*
import com.github.badoualy.telegram.tl.api.*
import com.google.gson.*
import com.github.salomonbrys.kotson.*
import org.slf4j.Logger
import org.slf4j.LoggerFactory
class CSVExporter(val db: Database, val file_base: String, val settings: Settings) {
val logger = LoggerFactory.getLogger(CSVExporter::class.java)
val mustache = DefaultMustacheFactory().compile("templates/csv/messages.csv")
val dialogs = db.getListOfDialogsForExport()
val chats = db.getListOfChatsForExport()
val datetime_format = SimpleDateFormat("yyyy-MM-dd HH:mm:ss")
val base = file_base + "files" + File.separatorChar
fun export() {
val today = LocalDateTime.of(LocalDate.now(), LocalTime.MIDNIGHT)
val timezone = ZoneOffset.systemDefault()
val days = if (settings.max_file_age==-1) 7 else settings.max_file_age
// Create base dir
logger.debug("Creating base dir")
File(base).mkdirs()
if (days > 0) {
for (dayOffset in days downTo 1) {
val day = today.minusDays(dayOffset.toLong())
val start = day.toEpochSecond(timezone.rules.getOffset(day))
val end = start + 24 * 60 * 60
val filename = base + "messages.${day.format(DateTimeFormatter.ISO_LOCAL_DATE)}.csv"
if (!File(file_base + filename).exists()) {
logger.debug("Range: {} to {}", start, end)
println("Processing messages for ${day}...")
exportToFile(start, end, filename)
}
}
} else {
println("Processing all messages...")
exportToFile(0, Long.MAX_VALUE, base + "messages.all.csv")
}
}
fun exportToFile(start: Long, end: Long, filename: String) {
val list = mutableListOf<Map<String, String?>>()
db.getMessagesForCSVExport(start, end) {data: HashMap<String, Any> ->
val scope = HashMap<String, String?>()
val timestamp = data["time"] as Time
scope.put("time", datetime_format.format(timestamp))
scope.put("username", if (data["user_username"]!=null) data["user_username"] as String else null)
if (data["source_type"]=="dialog") {
scope.put("chat_name", "@" + (dialogs.firstOrNull{it.id==data["source_id"]}?.username ?: ""))
} else {
scope.put("chat_name", chats.firstOrNull{it.id==data["source_id"]}?.name)
}
scope.put("message", data["message"] as String)
list.add(scope)
}
val writer = getWriter(filename)
mustache.execute(writer, mapOf("messages" to list))
writer.close()
}
private fun getWriter(filename: String): OutputStreamWriter {
logger.trace("Creating writer for file {}", filename.anonymize())
return OutputStreamWriter(FileOutputStream(filename), Charset.forName("UTF-8").newEncoder())
}
}

View File

@ -0,0 +1,139 @@
/* 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.exporter
import java.io.File
import java.io.PrintWriter
import java.io.OutputStreamWriter
import java.io.FileOutputStream
import java.nio.charset.Charset
import java.io.FileWriter
import java.io.IOException
import java.io.FileNotFoundException
import java.net.URL
import org.apache.commons.io.FileUtils
import java.util.LinkedList
import java.util.HashMap
import java.time.LocalDate
import java.time.LocalTime
import java.time.LocalDateTime
import java.time.ZoneOffset
import java.time.format.DateTimeFormatter
import java.sql.Time
import java.text.SimpleDateFormat
import com.github.mustachejava.DefaultMustacheFactory
import com.github.mustachejava.Mustache
import com.github.mustachejava.MustacheFactory
import de.fabianonline.telegram_backup.*
import com.github.badoualy.telegram.tl.api.*
import com.google.gson.*
import com.github.salomonbrys.kotson.*
import org.slf4j.Logger
import org.slf4j.LoggerFactory
class CSVLinkExporter(val db: Database, val file_base: String, val settings: Settings) {
val logger = LoggerFactory.getLogger(CSVLinkExporter::class.java)
val mustache = DefaultMustacheFactory().compile("templates/csv/links.csv")
val dialogs = db.getListOfDialogsForExport()
val chats = db.getListOfChatsForExport()
val datetime_format = SimpleDateFormat("yyyy-MM-dd HH:mm:ss")
val base = file_base + "files" + File.separatorChar
val invalid_entity_index = "[INVALID ENTITY INDEX]"
fun export() {
val today = LocalDateTime.of(LocalDate.now(), LocalTime.MIDNIGHT)
val timezone = ZoneOffset.systemDefault()
val days = if (settings.max_file_age==-1) 7 else settings.max_file_age
// Create base dir
logger.debug("Creating base dir")
File(base).mkdirs()
if (days > 0) {
for (dayOffset in days downTo 1) {
val day = today.minusDays(dayOffset.toLong())
val start = day.toEpochSecond(timezone.rules.getOffset(day))
val end = start + 24 * 60 * 60
val filename = base + "links.${day.format(DateTimeFormatter.ISO_LOCAL_DATE)}.csv"
if (!File(file_base + filename).exists()) {
logger.debug("Range: {} to {}", start, end)
println("Processing messages for ${day}...")
exportToFile(start, end, filename)
}
}
} else {
println("Processing all messages...")
exportToFile(0, Long.MAX_VALUE, base + "links.all.csv")
}
}
fun exportToFile(start: Long, end: Long, filename: String) {
//val messages: List<Map<String, Any>> = db.getMessagesForCSVExport(start, end)
val list = mutableListOf<Map<String, String?>>()
val parser = JsonParser()
//logger.debug("Got {} messages", messages.size)
db.getMessagesForCSVExport(start, end) {data: HashMap<String, Any> ->
//val msg: TLMessage = data.get("message_object") as TLMessage
val json = parser.parse(data.get("json") as String).obj
if (!json.contains("entities")) return@getMessagesForCSVExport
val urls: List<String>? = json["entities"].array.filter{it.obj.isA("messageEntityTextUrl") || it.obj.isA("messageEntityUrl")}?.map {
var url: String
try {
url = if (it.obj.contains("url")) it["url"].string else json["message"].string.substring(it["offset"].int, it["offset"].int + it["length"].int)
if (!url.toLowerCase().startsWith("http:") && !url.toLowerCase().startsWith("https://")) url = "http://${url}"
} catch (e: StringIndexOutOfBoundsException) {
url = invalid_entity_index
}
url
}
if (urls != null) for(url in urls) {
val scope = HashMap<String, String?>()
scope.put("url", url)
if (url == invalid_entity_index) {
scope.put("host", invalid_entity_index)
} else {
scope.put("host", URL(url).getHost())
}
val timestamp = data["time"] as Time
scope.put("time", datetime_format.format(timestamp))
scope.put("username", if (data["user_username"]!=null) data["user_username"] as String else null)
if (data["source_type"]=="dialog") {
scope.put("chat_name", "@" + (dialogs.firstOrNull{it.id==data["source_id"]}?.username ?: ""))
} else {
scope.put("chat_name", chats.firstOrNull{it.id==data["source_id"]}?.name)
}
list.add(scope)
}
}
val writer = getWriter(filename)
mustache.execute(writer, mapOf("links" to list))
writer.close()
}
private fun getWriter(filename: String): OutputStreamWriter {
logger.trace("Creating writer for file {}", filename.anonymize())
return OutputStreamWriter(FileOutputStream(filename), Charset.forName("UTF-8").newEncoder())
}
}

View File

@ -16,12 +16,6 @@
package de.fabianonline.telegram_backup.exporter
import de.fabianonline.telegram_backup.UserManager
import de.fabianonline.telegram_backup.Database
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.PrintWriter
import java.io.OutputStreamWriter
@ -38,22 +32,20 @@ import java.util.HashMap
import com.github.mustachejava.DefaultMustacheFactory
import com.github.mustachejava.Mustache
import com.github.mustachejava.MustacheFactory
import de.fabianonline.telegram_backup.*
import org.slf4j.Logger
import org.slf4j.LoggerFactory
class HTMLExporter {
val db = Database.getInstance()
val user = UserManager.getInstance()
class HTMLExporter(val db: Database, val user: UserManager, val settings: Settings, val file_base: String) {
@Throws(IOException::class)
fun export() {
try {
val pagination = if (CommandLineOptions.cmd_no_pagination) -1 else CommandLineOptions.val_pagination
val pagination = if (settings.pagination) settings.pagination_size else -1
// Create base dir
logger.debug("Creating base dir")
val base = user.fileBase + "files" + File.separatorChar
val base = file_base + "files" + File.separatorChar
File(base).mkdirs()
File(base + "dialogs").mkdirs()

View File

@ -17,56 +17,43 @@
package de.fabianonline.telegram_backup.mediafilemanager
import de.fabianonline.telegram_backup.UserManager
import de.fabianonline.telegram_backup.Database
import de.fabianonline.telegram_backup.StickerConverter
import de.fabianonline.telegram_backup.DownloadProgressInterface
import de.fabianonline.telegram_backup.Config
import de.fabianonline.telegram_backup.DownloadManager
import com.github.badoualy.telegram.api.TelegramClient
import com.github.badoualy.telegram.tl.core.TLIntVector
import com.github.badoualy.telegram.tl.core.TLObject
import com.github.badoualy.telegram.tl.api.messages.TLAbsMessages
import com.github.badoualy.telegram.tl.api.messages.TLAbsDialogs
import com.github.badoualy.telegram.tl.api.*
import com.github.badoualy.telegram.tl.api.upload.TLFile
import com.github.badoualy.telegram.tl.exception.RpcErrorException
import com.github.badoualy.telegram.tl.api.request.TLRequestUploadGetFile
import de.fabianonline.telegram_backup.Settings
import java.io.IOException
import java.io.File
import java.io.FileOutputStream
import java.util.ArrayList
import java.util.LinkedList
import java.net.URL
import java.util.concurrent.TimeoutException
import com.google.gson.*
import com.github.salomonbrys.kotson.*
import de.fabianonline.telegram_backup.*
import org.apache.commons.io.FileUtils
abstract class AbstractMediaFileManager(protected var message: TLMessage, protected var user: UserManager, protected var client: TelegramClient) {
abstract class AbstractMediaFileManager(private var json: JsonObject, val file_base: String) {
open var isEmpty = false
abstract val size: Int
abstract val extension: String
open val downloaded: Boolean
get() = File(targetPathAndFilename).isFile()
get() = !isEmpty && File(targetPathAndFilename).isFile()
val downloading: Boolean
get() = File("${targetPathAndFilename}.downloading").isFile()
open val targetPath: String
get() {
val path = user.fileBase + Config.FILE_FILES_BASE + File.separatorChar
val path = file_base + Config.FILE_FILES_BASE + File.separatorChar
File(path).mkdirs()
return path
}
open val targetFilename: String
get() {
val message_id = message.getId()
var to = message.getToId()
if (to is TLPeerChannel) {
val channel_id = to.getChannelId()
val message_id = json["id"].int
var to = json["toId"].obj
if (to.isA("peerChannel")) {
val channel_id = to["channelId"].int
return "channel_${channel_id}_${message_id}.$extension"
} else return "${message_id}.$extension"
}
@ -78,7 +65,7 @@ abstract class AbstractMediaFileManager(protected var message: TLMessage, protec
abstract val name: String
abstract val description: String
@Throws(RpcErrorException::class, IOException::class, TimeoutException::class)
abstract fun download(): Boolean
abstract fun download(prog: DownloadProgressInterface? = null): Boolean
protected fun extensionFromMimetype(mime: String): String {
when (mime) {
@ -93,8 +80,8 @@ abstract class AbstractMediaFileManager(protected var message: TLMessage, protec
}
companion object {
fun throwUnexpectedObjectError(o: Any) {
throw RuntimeException("Unexpected " + o.javaClass.getName())
fun throwUnexpectedObjectError(constructor: String) {
throw RuntimeException("Unexpected ${constructor}")
}
}
}

View File

@ -17,68 +17,45 @@
package de.fabianonline.telegram_backup.mediafilemanager
import de.fabianonline.telegram_backup.UserManager
import de.fabianonline.telegram_backup.Database
import de.fabianonline.telegram_backup.StickerConverter
import de.fabianonline.telegram_backup.DownloadProgressInterface
import de.fabianonline.telegram_backup.DownloadManager
import com.github.badoualy.telegram.api.TelegramClient
import com.github.badoualy.telegram.tl.core.TLIntVector
import com.github.badoualy.telegram.tl.core.TLObject
import com.github.badoualy.telegram.tl.api.messages.TLAbsMessages
import com.github.badoualy.telegram.tl.api.messages.TLAbsDialogs
import de.fabianonline.telegram_backup.DownloadProgressInterface
import com.github.badoualy.telegram.tl.api.*
import com.github.badoualy.telegram.tl.api.upload.TLFile
import com.github.badoualy.telegram.tl.exception.RpcErrorException
import com.github.badoualy.telegram.tl.api.request.TLRequestUploadGetFile
import java.io.IOException
import java.io.File
import java.io.FileOutputStream
import java.util.ArrayList
import java.util.LinkedList
import java.net.URL
import java.util.concurrent.TimeoutException
import com.google.gson.*
import com.github.salomonbrys.kotson.*
import de.fabianonline.telegram_backup.*
import org.apache.commons.io.FileUtils
open class DocumentFileManager(msg: TLMessage, user: UserManager, client: TelegramClient) : AbstractMediaFileManager(msg, user, client) {
protected var doc: TLDocument? = null
open class DocumentFileManager(message: JsonObject, file_base: String) : AbstractMediaFileManager(message, file_base) {
//protected var doc: TLDocument? = null
override lateinit var extension: String
open val isSticker: Boolean
get() {
if (this.isEmpty || doc == null) return false
return doc!!.getAttributes()?.filter { it is TLDocumentAttributeSticker }?.isNotEmpty() ?: false
}
get() = json.get("attributes")?.array?.any{it.obj.isA("documentAttributeSticker")} ?: false
override val size: Int
get() = if (doc != null) doc!!.getSize() else 0
get() = json["size"].int
open override val letter: String = "d"
open override val name: String = "document"
open override val description: String = "Document"
private val json = message["media"]["document"].obj
init {
val d = (msg.getMedia() as TLMessageMediaDocument).getDocument()
if (d is TLDocument) {
this.doc = d
} else if (d is TLDocumentEmpty) {
this.isEmpty = true
} else {
throwUnexpectedObjectError(d)
}
extension = processExtension()
}
private fun processExtension(): String {
if (doc == null) return "empty"
//if (doc == null) return "empty"
var ext: String? = null
var original_filename: String? = null
if (doc!!.getAttributes() != null)
for (attr in doc!!.getAttributes()) {
if (attr is TLDocumentAttributeFilename) {
original_filename = attr.getFileName()
if (json.contains("attributes"))
for (attr in json["attributes"].array) {
if (attr.obj["_constructor"].string.startsWith("documentAttributeFilename")) {
original_filename = attr.obj["fileName"].string
}
}
if (original_filename != null) {
@ -87,7 +64,7 @@ open class DocumentFileManager(msg: TLMessage, user: UserManager, client: Telegr
}
if (ext == null) {
ext = extensionFromMimetype(doc!!.getMimeType())
ext = extensionFromMimetype(json["mimeType"].string)
}
// Sometimes, extensions contain a trailing double quote. Remove this. Fixes #12.
@ -97,10 +74,8 @@ open class DocumentFileManager(msg: TLMessage, user: UserManager, client: Telegr
}
@Throws(RpcErrorException::class, IOException::class, TimeoutException::class)
override fun download(): Boolean {
if (doc != null) {
DownloadManager.downloadFile(targetPathAndFilename, size, doc!!.getDcId(), doc!!.getId(), doc!!.getAccessHash())
}
override fun download(prog: DownloadProgressInterface?): Boolean {
DownloadManager.downloadFile(targetPathAndFilename, size, json["dcId"].int, json["id"].long, json["accessHash"].long, prog)
return true
}
}

View File

@ -18,7 +18,6 @@ package de.fabianonline.telegram_backup.mediafilemanager
import de.fabianonline.telegram_backup.UserManager
import de.fabianonline.telegram_backup.Database
import de.fabianonline.telegram_backup.StickerConverter
import de.fabianonline.telegram_backup.DownloadProgressInterface
import com.github.badoualy.telegram.api.TelegramClient
@ -30,44 +29,62 @@ import com.github.badoualy.telegram.tl.api.*
import com.github.badoualy.telegram.tl.api.upload.TLFile
import com.github.badoualy.telegram.tl.exception.RpcErrorException
import com.github.badoualy.telegram.tl.api.request.TLRequestUploadGetFile
import de.fabianonline.telegram_backup.Settings
import java.io.IOException
import java.io.File
import java.io.FileOutputStream
import java.util.ArrayList
import java.util.LinkedList
import java.util.NoSuchElementException
import java.net.URL
import java.util.concurrent.TimeoutException
import com.google.gson.*
import com.github.salomonbrys.kotson.*
import de.fabianonline.telegram_backup.*
import org.apache.commons.io.FileUtils
object FileManagerFactory {
fun getFileManager(m: TLMessage?, u: UserManager, c: TelegramClient): AbstractMediaFileManager? {
fun getFileManager(m: TLMessage?, file_base: String, settings: Settings?): AbstractMediaFileManager? {
if (m == null) return null
val media = m.getMedia() ?: return null
if (media is TLMessageMediaPhoto) {
return PhotoFileManager(m, u, c)
} else if (media is TLMessageMediaDocument) {
val d = DocumentFileManager(m, u, c)
return if (d.isSticker) {
StickerFileManager(m, u, c)
} else d
} else if (media is TLMessageMediaGeo) {
return GeoFileManager(m, u, c)
} else if (media is TLMessageMediaEmpty) {
return UnsupportedFileManager(m, u, c, "empty")
} else if (media is TLMessageMediaUnsupported) {
return UnsupportedFileManager(m, u, c, "unsupported")
} else if (media is TLMessageMediaWebPage) {
return UnsupportedFileManager(m, u, c, "webpage")
} else if (media is TLMessageMediaContact) {
return UnsupportedFileManager(m, u, c, "contact")
} else if (media is TLMessageMediaVenue) {
return UnsupportedFileManager(m, u, c, "venue")
} else {
AbstractMediaFileManager.throwUnexpectedObjectError(media)
val json = Gson().toJsonTree(m).obj
return getFileManager(json, file_base, settings)
}
fun getFileManager(message: JsonObject?, file_base: String, settings: Settings?): AbstractMediaFileManager? {
if (message == null) return null
try {
val media = message.get("media")?.obj ?: return null
if (media.isA("messageMediaPhoto")) {
return PhotoFileManager(message, file_base)
} else if (media.isA("messageMediaDocument")) {
val d = DocumentFileManager(message, file_base)
return if (d.isSticker) StickerFileManager(message, file_base) else d
} else if (media.isA("messageMediaGeo")) {
return GeoFileManager(message, file_base, settings)
} else if (media.isA("messageMediaEmpty")) {
return UnsupportedFileManager(message, file_base, "empty")
} else if (media.isA("messageMediaUnsupported")) {
return UnsupportedFileManager(message, file_base, "unsupported")
} else if (media.isA("messageMediaWebPage")) {
return UnsupportedFileManager(message, file_base, "webpage")
} else if (media.isA("messageMediaContact")) {
return UnsupportedFileManager(message, file_base, "contact")
} else if (media.isA("messageMediaVenue")) {
return UnsupportedFileManager(message, file_base, "venue")
} else {
AbstractMediaFileManager.throwUnexpectedObjectError(media["_constructor"].string)
}
} catch (e: IllegalStateException) {
println(message.toPrettyJson())
throw e
} catch (e: NoSuchElementException) {
println(message.toPrettyJson())
throw e
}
return null
}
}

View File

@ -17,34 +17,20 @@
package de.fabianonline.telegram_backup.mediafilemanager
import de.fabianonline.telegram_backup.UserManager
import de.fabianonline.telegram_backup.Database
import de.fabianonline.telegram_backup.StickerConverter
import de.fabianonline.telegram_backup.DownloadProgressInterface
import de.fabianonline.telegram_backup.DownloadManager
import de.fabianonline.telegram_backup.Config
import com.github.badoualy.telegram.api.TelegramClient
import com.github.badoualy.telegram.tl.core.TLIntVector
import com.github.badoualy.telegram.tl.core.TLObject
import com.github.badoualy.telegram.tl.api.messages.TLAbsMessages
import com.github.badoualy.telegram.tl.api.messages.TLAbsDialogs
import de.fabianonline.telegram_backup.DownloadProgressInterface
import com.github.badoualy.telegram.tl.api.*
import com.github.badoualy.telegram.tl.api.upload.TLFile
import com.github.badoualy.telegram.tl.exception.RpcErrorException
import com.github.badoualy.telegram.tl.api.request.TLRequestUploadGetFile
import de.fabianonline.telegram_backup.Config
import de.fabianonline.telegram_backup.Settings
import java.io.IOException
import java.io.File
import java.io.FileOutputStream
import java.util.ArrayList
import java.util.LinkedList
import java.net.URL
import java.util.concurrent.TimeoutException
import com.google.gson.*
import com.github.salomonbrys.kotson.*
import de.fabianonline.telegram_backup.Utils
import org.apache.commons.io.FileUtils
class GeoFileManager(msg: TLMessage, user: UserManager, client: TelegramClient) : AbstractMediaFileManager(msg, user, client) {
protected lateinit var geo: TLGeoPoint
class GeoFileManager(message: JsonObject, file_base: String, val settings: Settings?) : AbstractMediaFileManager(message, file_base) {
//protected lateinit var geo: TLGeoPoint
// We don't know the size, so we just guess.
override val size: Int
@ -59,8 +45,11 @@ class GeoFileManager(msg: TLMessage, user: UserManager, client: TelegramClient)
override val letter = "g"
override val name = "geo"
override val description = "Geolocation"
val json = message["media"]["geo"].obj
init {
/*
val g = (msg.getMedia() as TLMessageMediaGeo).getGeo()
if (g is TLGeoPoint) {
this.geo = g
@ -68,15 +57,16 @@ class GeoFileManager(msg: TLMessage, user: UserManager, client: TelegramClient)
this.isEmpty = true
} else {
throwUnexpectedObjectError(g)
}
}*/
}
@Throws(IOException::class)
override fun download(): Boolean {
override fun download(prog: DownloadProgressInterface?): Boolean {
val url = "https://maps.googleapis.com/maps/api/staticmap?" +
"center=" + geo.getLat() + "," + geo.getLong() + "&" +
"center=${json["lat"].float},${json["_long"].float}&" +
"markers=color:red|${json["lat"].float},${json["_long"].float}&" +
"zoom=14&size=300x150&scale=2&format=png&" +
"key=" + Config.SECRET_GMAPS
return DownloadManager.downloadExternalFile(targetPathAndFilename, url)
"key=" + (settings?.gmaps_key)
return DownloadManager.downloadExternalFile(targetPathAndFilename, url, prog)
}
}

View File

@ -17,70 +17,71 @@
package de.fabianonline.telegram_backup.mediafilemanager
import de.fabianonline.telegram_backup.UserManager
import de.fabianonline.telegram_backup.Database
import de.fabianonline.telegram_backup.StickerConverter
import de.fabianonline.telegram_backup.DownloadProgressInterface
import de.fabianonline.telegram_backup.DownloadManager
import com.github.badoualy.telegram.api.TelegramClient
import com.github.badoualy.telegram.tl.core.TLIntVector
import com.github.badoualy.telegram.tl.core.TLObject
import com.github.badoualy.telegram.tl.api.messages.TLAbsMessages
import com.github.badoualy.telegram.tl.api.messages.TLAbsDialogs
import de.fabianonline.telegram_backup.DownloadProgressInterface
import com.github.badoualy.telegram.tl.api.*
import com.github.badoualy.telegram.tl.api.upload.TLFile
import com.github.badoualy.telegram.tl.exception.RpcErrorException
import com.github.badoualy.telegram.tl.api.request.TLRequestUploadGetFile
import java.io.IOException
import java.io.File
import java.io.FileOutputStream
import java.util.ArrayList
import java.util.LinkedList
import java.net.URL
import java.util.concurrent.TimeoutException
import org.apache.commons.io.FileUtils
import com.google.gson.*
import com.github.salomonbrys.kotson.*
import de.fabianonline.telegram_backup.*
class PhotoFileManager(msg: TLMessage, user: UserManager, client: TelegramClient) : AbstractMediaFileManager(msg, user, client) {
private lateinit var photo: TLPhoto
class PhotoFileManager(message: JsonObject, file_base: String) : AbstractMediaFileManager(message, file_base) {
//private lateinit var photo: TLPhoto
override var size = 0
private lateinit var photo_size: TLPhotoSize
//private lateinit var photo_size: TLPhotoSize
override val extension = "jpg"
override val letter = "p"
override val name = "photo"
override val description = "Photo"
val biggestSize: JsonObject
var biggestSizeW = 0
var biggestSizeH = 0
val json = message["media"]["photo"].obj
override var isEmpty = json.isA("photoEmpty")
init {
val p = (msg.getMedia() as TLMessageMediaPhoto).getPhoto()
if (p is TLPhoto) {
this.photo = p
var biggest: TLPhotoSize? = null
for (s in photo.getSizes())
if (s is TLPhotoSize) {
if (biggest == null || s.getW() > biggest.getW() && s.getH() > biggest.getH()) {
biggest = s
}
/*val p = (msg.getMedia() as TLMessageMediaPhoto).getPhoto()*/
if (!isEmpty) {
var bsTemp: JsonObject? = null
for (elm in json["sizes"].array) {
val s = elm.obj
if (!s.isA("photoSize")) continue
if (bsTemp == null || (s["w"].int > biggestSizeW && s["h"].int > biggestSizeH)) {
bsTemp = s
biggestSizeW = s["w"].int
biggestSizeH = s["h"].int
size = s["size"].int // file size
}
if (biggest == null) {
throw RuntimeException("Could not find a size for a photo.")
}
this.photo_size = biggest
this.size = biggest.getSize()
} else if (p is TLPhotoEmpty) {
if (bsTemp == null) throw RuntimeException("Could not find a size for a photo.")
biggestSize = bsTemp
} else {
biggestSize = JsonObject()
}
/*} else if (p is TLPhotoEmpty) {
this.isEmpty = true
} else {
throwUnexpectedObjectError(p)
}
}*/
}
@Throws(RpcErrorException::class, IOException::class, TimeoutException::class)
override fun download(): Boolean {
if (isEmpty) return true
val loc = photo_size.getLocation() as TLFileLocation
DownloadManager.downloadFile(targetPathAndFilename, size, loc.getDcId(), loc.getVolumeId(), loc.getLocalId(), loc.getSecret())
override fun download(prog: DownloadProgressInterface?): Boolean {
/*if (isEmpty) return true*/
//val loc = photo_size.getLocation() as TLFileLocation
val loc = biggestSize["location"].obj
DownloadManager.downloadFile(targetPathAndFilename, size, loc["dcId"].int, loc["volumeId"].long, loc["localId"].int, loc["secret"].long, prog)
return true
}
}

View File

@ -18,7 +18,6 @@ package de.fabianonline.telegram_backup.mediafilemanager
import de.fabianonline.telegram_backup.UserManager
import de.fabianonline.telegram_backup.Database
import de.fabianonline.telegram_backup.StickerConverter
import de.fabianonline.telegram_backup.DownloadProgressInterface
import de.fabianonline.telegram_backup.DownloadManager
import de.fabianonline.telegram_backup.Config
@ -50,29 +49,23 @@ import java.util.concurrent.TimeoutException
import org.apache.commons.io.FileUtils
class StickerFileManager(msg: TLMessage, user: UserManager, client: TelegramClient) : DocumentFileManager(msg, user, client) {
import com.google.gson.*
import com.github.salomonbrys.kotson.*
import de.fabianonline.telegram_backup.*
class StickerFileManager(message: JsonObject, file_base: String) : DocumentFileManager(message, file_base) {
override val isSticker = true
val json = message["media"]["document"].obj
val sticker = json["attributes"].array.first{it.obj.isA("documentAttributeSticker")}.obj
override var isEmpty = sticker["stickerset"].obj.isA("inputStickerSetEmpty")
private val filenameBase: String
get() {
var sticker: TLDocumentAttributeSticker? = null
for (attr in doc!!.getAttributes()) {
if (attr is TLDocumentAttributeSticker) {
sticker = attr
}
}
val file = StringBuilder()
val set = sticker!!.getStickerset()
if (set is TLInputStickerSetShortName) {
file.append(set.getShortName())
} else if (set is TLInputStickerSetID) {
file.append(set.getId())
}
file.append("_")
file.append(sticker.getAlt().hashCode())
return file.toString()
val set = sticker["stickerset"].obj.get("shortName").nullString ?: sticker["stickerset"].obj.get("id").string
val hash = sticker["alt"].string.hashCode()
return "${set}_${hash}"
}
override val targetFilename: String
@ -80,7 +73,7 @@ class StickerFileManager(msg: TLMessage, user: UserManager, client: TelegramClie
override val targetPath: String
get() {
val path = user.fileBase + Config.FILE_FILES_BASE + File.separatorChar + Config.FILE_STICKER_BASE + File.separatorChar
val path = file_base + Config.FILE_FILES_BASE + File.separatorChar + Config.FILE_STICKER_BASE + File.separatorChar
File(path).mkdirs()
return path
}
@ -94,19 +87,6 @@ class StickerFileManager(msg: TLMessage, user: UserManager, client: TelegramClie
override val description: String
get() = "Sticker"
@Throws(RpcErrorException::class, IOException::class, TimeoutException::class)
override fun download(): Boolean {
val old_file = Config.FILE_BASE + File.separatorChar + Config.FILE_STICKER_BASE + File.separatorChar + targetFilename
logger.trace("Old filename exists: {}", File(old_file).exists())
if (File(old_file).exists()) {
Files.copy(Paths.get(old_file), Paths.get(targetPathAndFilename), StandardCopyOption.REPLACE_EXISTING)
return true
}
return super.download()
}
companion object {
private val logger = LoggerFactory.getLogger(StickerFileManager::class.java)
}

View File

@ -17,33 +17,11 @@
package de.fabianonline.telegram_backup.mediafilemanager
import de.fabianonline.telegram_backup.UserManager
import de.fabianonline.telegram_backup.Database
import de.fabianonline.telegram_backup.StickerConverter
import de.fabianonline.telegram_backup.DownloadProgressInterface
import de.fabianonline.telegram_backup.DownloadManager
import de.fabianonline.telegram_backup.Config
import com.github.badoualy.telegram.api.TelegramClient
import com.github.badoualy.telegram.tl.core.TLIntVector
import com.github.badoualy.telegram.tl.core.TLObject
import com.github.badoualy.telegram.tl.api.messages.TLAbsMessages
import com.github.badoualy.telegram.tl.api.messages.TLAbsDialogs
import com.github.badoualy.telegram.tl.api.*
import com.github.badoualy.telegram.tl.api.upload.TLFile
import com.github.badoualy.telegram.tl.exception.RpcErrorException
import com.github.badoualy.telegram.tl.api.request.TLRequestUploadGetFile
import com.google.gson.JsonObject
import java.io.IOException
import java.io.File
import java.io.FileOutputStream
import java.util.ArrayList
import java.util.LinkedList
import java.net.URL
import java.util.concurrent.TimeoutException
import org.apache.commons.io.FileUtils
class UnsupportedFileManager(msg: TLMessage, user: UserManager, client: TelegramClient, type: String) : AbstractMediaFileManager(msg, user, client) {
class UnsupportedFileManager(json: JsonObject, file_base: String, type: String) : AbstractMediaFileManager(json, file_base) {
override var name = type
override val targetFilename = ""
override val targetPath = ""
@ -54,5 +32,5 @@ class UnsupportedFileManager(msg: TLMessage, user: UserManager, client: Telegram
override val letter = " "
override val description = "Unsupported / non-downloadable Media"
override fun download(): Boolean = true
override fun download(prog: DownloadProgressInterface?): Boolean = true
}

View File

@ -0,0 +1,71 @@
# Config file for telegram_backup
# Copy it to config.ini to use it. This sample file be overwritten on every run.
#
# Lines starting with '#' are ignored.
# Settings have the form 'key = value'
# To unset a setting, use 'key =' (or just don't use that key at all)
# Some settings may appear more than once.
## GMaps key to use. Leave empty / don't set a value to use the built in key.
# gmaps_key = mysecretgmapskey
## Use pagination in the HTML export?
# pagination = true
# pagination_size = 5000
## Download media files
# download_media = true
## Only download media files from messages that are never than x days.
## Leave unset to download all media files.
# max_file_age = 7
## Only download media files that are smaller than x MB.
## Leave unset to download media files regardless of their size.
# max_file_size = 5
## Don't download media files having these extensions.
## You can add this line multiple times to blacklist more than one extension.
# blacklist_extensions = jpg
# blacklist_extensions = avi
## Downloads of channels and supergroups
## Here you can specify which channels and supergroups
## should be downloaded. The rules for this are:
## 1. Channels and supergroups are NEVER downloaded unless download_channels and/or
## download_supergroups is set to true.
## 2. If there is at least one entry called whitelist_channels, ONLY channels and/or
## supergroups that are listed in the whitelist will be downloaded.
## 3. Only if there are NO channels whitelisted, entrys listed as blacklist_channels
## will not be downloaded, all other channels / supergroups will be.
##
## In other words:
## * Set download_channels and/or download_supergroups to true if you want to include
## those types in your backup.
## * If you use neither black- nor whitelist, all channels (if you set download_channels
## to true) / supergroups (if you set download_supergroups to true) get downloaded.
## * If you set a whitelist, only listed channels / supergroups (there is no distinction
## made here) will be loaded.
## * If you set a blacklist, everything except the listed channels / supergroups (again,
## although the entry is called whitelist_channels it affects channels AND supergroups)
## will be loaded.
## * If you set a whitelist AND a blacklist, the blacklist will be ignored.
##
## Call the app with `--list-channels` to list the available channels and supergroups
## with their IDs. That list will also tell you if a channel / supergroup will be
## be downloaded according to your black- and whitelists.
##
## You can have more than one whitelist_channels and/or blacklist_channels entries
## to build your list. One ID per entry.
# download_channels = false
# download_supergroups = false
# blacklist_channels = 12345678
# blacklist_channels = 8886542
# blacklist_channels = 715952
# whitelist_channels = 238572935
# whitelist_channels = 23857623

View File

@ -0,0 +1,3 @@
{{#links}}
"{{time}}","{{url}}","{{host}}","{{username}}","{{chat_name}}"
{{/links}}
Can't render this file because it has a wrong number of fields in line 2.

View File

@ -0,0 +1,3 @@
{{#messages}}
"{{time}}","{{username}}","{{chat_name}}","{{message}}"
{{/messages}}
Can't render this file because it has a wrong number of fields in line 2.

View File

@ -12,6 +12,7 @@
{{#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}}
{{#media_geo}}<span class="geo"><img src="../{{media_file}}" width="300" height="150"></span>{{/media_geo}}
</li>
{{/messages}}
</ul>