Skip to content

Commit ea36767

Browse files
committed
add method to use package as a cli command
1 parent 3a690cb commit ea36767

File tree

10 files changed

+773
-23
lines changed

10 files changed

+773
-23
lines changed

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
.idea
2-
__pycache__
2+
__pycache__
3+
dist

README.md

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,53 @@
1-
## Telegram export tool.
1+
## Telegram export tool.
2+
3+
### Installation
4+
```shell
5+
$ pip install t-export
6+
```
7+
8+
### Usage
9+
```shell
10+
Usage: python -m texport [OPTIONS]
11+
12+
Options:
13+
--api-id INTEGER Telegram api id. Saved in
14+
~/.texport/config.json file.
15+
--api-hash TEXT Telegram api hash. Saved in
16+
~/.texport/config.json file.
17+
-s, --session-name TEXT Pyrogram session name or path to existing file.
18+
Saved in ~/.texport/<session_name>.session file.
19+
-c, --chat-id TEXT Chat id or username or phone number. "me" or
20+
"self" to export saved messages.
21+
-o, --output TEXT Output directory.
22+
-l, --size-limit INTEGER Media size limit in megabytes.
23+
-f, --from-date TEXT Date from which messages will be saved.
24+
-t, --to-date TEXT Date to which messages will be saved.
25+
--photos / --no-photos Download photos or not.
26+
--videos / --no-videos Download videos or not.
27+
--voice / --no-voice Download voice messages or not.
28+
--video-notes / --no-video-notes
29+
Download video messages or not.
30+
--stickers / --no-stickers Download stickers or not.
31+
--gifs / --no-gifs Download gifs or not.
32+
--documents / --no-documents Download documents or not.
33+
--quiet BOOLEAN Do not print progress to console.
34+
```
35+
At first run you will need to specify api id and api hash and log in into your telegram account.
36+
Or you can pass path of existing pyrogram session to "--session" argument (no need to logging in or specifying api id or api hash).
37+
38+
### Examples
39+
40+
#### Export all messages from private chat with user @example to directory example_export
41+
```shell
42+
$ t-export -c example -o example export
43+
```
44+
45+
#### Export all messages from private chat with user @example to directory example_export without videos and with size limit of 100 megabytes
46+
```shell
47+
$ t-export -c example -o example export --no-videos --size-limit 100
48+
```
49+
50+
#### Export all messages from start of 2023 from private chat with user @example to directory example_export
51+
```shell
52+
$ t-export -c example -o example export --size-limit 100 --from-date 01.01.2023
53+
```

poetry.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,43 @@
11
[tool.poetry]
22
name = "t-export"
33
version = "0.1.0"
4-
description = ""
4+
description = "Telegram chats export tool."
55
authors = ["RuslanUC <dev_ruslan_uc@protonmail.com>"]
66
readme = "README.md"
7+
license = "MIT"
8+
classifiers = [
9+
"Environment :: Console",
10+
"Framework :: AsyncIO",
11+
"Intended Audience :: Developers",
12+
"License :: OSI Approved :: MIT License",
13+
"Operating System :: OS Independent",
14+
"Programming Language :: Python",
15+
"Programming Language :: Python :: 3",
16+
"Programming Language :: Python :: 3 :: Only",
17+
"Programming Language :: Python :: 3.9",
18+
"Programming Language :: Python :: 3.10",
19+
"Programming Language :: Python :: 3.11",
20+
"Typing :: Typed",
21+
"Topic :: Internet",
22+
]
23+
packages = [
24+
{ include = "texport" }
25+
]
26+
27+
[tool.poetry.urls]
28+
Homepage = "https://github.com/RuslanUC/t-export"
29+
Repository = "https://github.com/RuslanUC/t-export"
30+
31+
[tool.poetry.scripts]
32+
texport = "texport.main:main"
33+
t_export = "texport.main:main"
734

835
[tool.poetry.dependencies]
936
python = "^3.9"
1037
pyrogram = "^2.0.106"
1138
tgcrypto = "^1.2.5"
1239
click = "^8.1.7"
40+
colorama = "^0.4.6"
1341

1442

1543
[build-system]

texport/export_config.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from dataclasses import dataclass
22
from datetime import datetime
3+
from pathlib import Path
34
from typing import Union
45

56
from pyrogram.enums import MessageMediaType
@@ -18,7 +19,7 @@
1819
@dataclass
1920
class ExportConfig:
2021
chat_id: Union[str, int] = "me"
21-
output_dir: str = "./telegram_export"
22+
output_dir: Path = Path("./telegram_export")
2223
export_photos: bool = True
2324
export_videos: bool = True
2425
export_voice: bool = True
@@ -29,6 +30,7 @@ class ExportConfig:
2930
size_limit: int = 32 # In megabytes
3031
from_date: datetime = datetime(1970, 1, 1)
3132
to_date: datetime = datetime.now()
33+
print: bool = False
3234

3335
def excluded_media(self) -> set[MessageMediaType]:
3436
result = set()

texport/exporter.py

Lines changed: 26 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,17 @@
55

66
from pyrogram import Client
77
from pyrogram.types import Message as PyroMessage
8+
from pyrogram.utils import zero_datetime
89

910
from texport.export_config import ExportConfig
1011
from texport.media import MEDIA_TYPES
1112
from texport.messages_saver import MessagesSaver
13+
from texport.progress_print import ProgressPrint
1214

1315

1416
class ExportStatus:
1517
def __init__(self):
16-
self.biggest_message_id = None
18+
self.approx_messages_count = None
1719
self.last_message_id = None
1820
self.last_date = None
1921

@@ -24,6 +26,7 @@ def __init__(self, client: Client, export_config: ExportConfig=None):
2426
self._client = client
2527
self._task = None
2628
self.status: Optional[ExportStatus] = None
29+
self._progress: ProgressPrint = ProgressPrint(disabled=not self._config.print)
2730
self._messages: list[PyroMessage] = []
2831
self._media: dict[Union[int, str], str] = {}
2932
self._saver = MessagesSaver(self._messages, self._media, export_config)
@@ -37,40 +40,53 @@ async def _export_media(self, message: PyroMessage) -> None:
3740
if media.file_size > self._config.size_limit * 1024 * 1024:
3841
return
3942

40-
path = await message.download(file_name=f"{self._config.output_dir}/{m.dir_name}/")
41-
path = relpath(path, self._config.output_dir)
43+
path = await message.download(file_name=f"{self._config.output_dir.absolute()}/{m.dir_name}/")
44+
path = relpath(path, self._config.output_dir.absolute())
4245
self._media[message.id] = path
4346

4447
if hasattr(media, "thumbs") and media.thumbs:
4548
path = await self._client.download_media(media.thumbs[0].file_id,
46-
file_name=f"{self._config.output_dir}/thumbs/")
47-
path = relpath(path, self._config.output_dir)
49+
file_name=f"{self._config.output_dir.absolute()}/thumbs/")
50+
path = relpath(path, self._config.output_dir.absolute())
4851
self._media[f"{message.id}_thumb"] = path
4952

5053
async def _export(self, chat_id: Union[int, str]):
51-
offset_date = None if self._config.to_date.date() >= date.today() else self._config.to_date
52-
# TODO: fix offset_date
53-
async for message in self._client.get_chat_history(chat_id):
54+
offset_date = zero_datetime() if self._config.to_date.date() >= date.today() else self._config.to_date
55+
loaded = 0
56+
self._progress.approx_messages_count = await self._client.get_chat_history_count(chat_id)
57+
async for message in self._client.get_chat_history(chat_id, offset_date=offset_date):
5458
if message.date < self._config.from_date:
5559
break
56-
if self.status.biggest_message_id is None:
57-
self.status.biggest_message_id = message.id
60+
61+
loaded += 1
62+
with self._progress.update():
63+
self._progress.status = "Exporting messages..."
64+
self._progress.messages_exported = loaded
65+
66+
if self.status.approx_messages_count is None:
67+
self.status.approx_messages_count = message.id
5868
self.status.last_message_id = message.id
5969
self.status.last_date = message.date
70+
6071
if message.media:
72+
self._progress.status = "Downloading media..."
6173
await self._export_media(message)
6274

6375
if not message.text and not message.caption and message.id not in self._media:
6476
continue
6577

6678
self._messages.append(message)
6779
if len(self._messages) > 5000:
80+
self._progress.status = "Writing messages to file..."
6881
await self._saver.save()
6982

7083
if self._messages:
84+
self._progress.status = "Writing messages to file..."
7185
await self._saver.save()
7286
self.status = self._task = None
7387

88+
self._progress.status = "Done!"
89+
7490
async def export(self, block: bool=True) -> None:
7591
if self._task is not None or self.status is not None:
7692
return

texport/main.py

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -44,18 +44,20 @@ async def _main(session_name: str, api_id: int, api_hash: str, config: ExportCon
4444
@click.option("--stickers/--no-stickers", default=True, help="Download stickers or not.")
4545
@click.option("--gifs/--no-gifs", default=True, help="Download gifs or not.")
4646
@click.option("--documents/--no-documents", default=True, help="Download documents or not.")
47+
@click.option("--quiet", default=False, help="Do not print progress to console.")
4748
def main(
4849
session_name: str, api_id: int, api_hash: str, chat_id: str, output: str, size_limit: int, from_date: str,
4950
to_date: str, photos: bool, videos: bool, voice: bool, video_notes: bool, stickers: bool, gifs: bool,
50-
documents: bool,
51+
documents: bool, quiet: bool,
5152
) -> None:
52-
texport_dir = Path.home() / ".texport"
53+
home = Path.home()
54+
texport_dir = home / ".texport"
5355
makedirs(texport_dir, exist_ok=True)
5456
makedirs(output, exist_ok=True)
5557

5658
config = ExportConfig(
5759
chat_id=chat_id,
58-
output_dir=output,
60+
output_dir=Path(output),
5961
size_limit=size_limit,
6062
from_date=datetime.strptime(from_date, "%d.%m.%Y"),
6163
to_date=datetime.strptime(to_date, "%d.%m.%Y"),
@@ -66,21 +68,23 @@ def main(
6668
export_stickers=stickers,
6769
export_gifs=gifs,
6870
export_files=documents,
71+
print=not quiet,
6972
)
7073

7174
if session_name.endswith(".session"):
7275
name = Path(session_name).name
73-
copy(session_name, f"~/.texport/{name}")
76+
copy(session_name, home / ".texport" / name)
7477
session_name = name[:8]
7578

76-
if api_id is None or api_hash is None:
79+
if (api_id is None or api_hash is None) and not exists(home / ".texport" / f"{session_name}.session"):
7780
if not exists(texport_dir / "config.json"):
78-
print("You should specify --api-id and --api-hash parameters!")
81+
print("You should specify \"--api-id\" and \"--api-hash\" arguments or import existing pyrogram session "
82+
"file by passing it's path to \"--session\" argument!")
7983
return
8084
with open(texport_dir / "config.json", "r", encoding="utf8") as f:
8185
conf = json.load(f)
8286
api_id, api_hash = conf["api_id"], conf["api_hash"]
83-
else:
87+
elif api_id is not None and api_hash is not None:
8488
with open(texport_dir / "config.json", "w", encoding="utf8") as f:
8589
json.dump({"api_id": api_id, "api_hash": api_hash}, f)
8690

texport/messages_saver.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
from asyncio import get_running_loop
2+
from os.path import exists
23
from typing import Union, Optional
34

45
from pyrogram.types import Message as PyroMessage
56

67
from .export_config import ExportConfig
78
from .html.base import Export
89
from .html.message import DateMessage, Message
10+
from .resources import unpack_to
911

1012

1113
class MessagesSaver:
@@ -16,6 +18,10 @@ def __init__(self, messages: list[PyroMessage], media: dict[Union[int, str], str
1618
self.config = config
1719

1820
def _save(self) -> None:
21+
out_dir = self.config.output_dir
22+
if not exists(out_dir / "js") or exists(out_dir / "images") or exists(out_dir / "css"):
23+
unpack_to(out_dir)
24+
1925
output = ""
2026
prev: Optional[PyroMessage] = None
2127
dates = 0
@@ -33,7 +39,7 @@ def _save(self) -> None:
3339
prev = message
3440

3541
output = Export(prev.chat.first_name, output).to_html()
36-
with open(f"{self.config.output_dir}/messages{self.part}.html", "w", encoding="utf8") as f:
42+
with open(f"{out_dir}/messages{self.part}.html", "w", encoding="utf8") as f:
3743
f.write(output)
3844

3945
async def save(self) -> None:

0 commit comments

Comments
 (0)