commit 3b1178e7d7226ca2f00c605cee925b3476f556ed Author: odysseusmax Date: Mon Aug 10 07:27:52 2020 +0000 initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..adb9b40 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +*.session +.env +__pycache__/ +venv/ diff --git a/Procfile b/Procfile new file mode 100644 index 0000000..364f290 --- /dev/null +++ b/Procfile @@ -0,0 +1 @@ +web: python3 -m app diff --git a/app/__main__.py b/app/__main__.py new file mode 100644 index 0000000..5d6a810 --- /dev/null +++ b/app/__main__.py @@ -0,0 +1,3 @@ +from .main import main + +main() diff --git a/app/config.py b/app/config.py new file mode 100644 index 0000000..4838b2b --- /dev/null +++ b/app/config.py @@ -0,0 +1,33 @@ +import sys +import os + + +try: + port = int(os.environ.get("PORT", "8080")) +except ValueError: + port = -1 +if not 1 <= port <= 65535: + print("Please make sure the PORT environment variable is an integer between 1 and 65535") + sys.exit(1) + +try: + api_id = int(os.environ["API_ID"]) + api_hash = os.environ["API_HASH"] +except (KeyError, ValueError): + print("Please set the API_ID and API_HASH environment variables correctly") + print("You can get your own API keys at https://my.telegram.org/apps") + sys.exit(1) + +try: + chat_id = int(os.environ["CHAT_ID"]) +except (KeyError, ValueError): + print("Please set the CHAT_ID environment variable correctly") + sys.exit(1) + +try: + session_string = os.environ["SESSION_STRING"] +except (KeyError, ValueError): + print("Please set the SESSION_STRING environment variable correctly") + sys.exit(1) + +host = os.environ.get("HOST", "0.0.0.0") diff --git a/app/main.py b/app/main.py new file mode 100644 index 0000000..32c7099 --- /dev/null +++ b/app/main.py @@ -0,0 +1,42 @@ +import asyncio +import pathlib + +import aiohttp_jinja2 +import jinja2 +from aiohttp import web + +from .telegram import Client +from .routes import setup_routes +from .views import Views +from .config import host, port, session_string, api_id, api_hash + +TEMPLATES_ROOT = pathlib.Path(__file__).parent / 'templates' +client = Client(session_string, api_id, api_hash) + + +def setup_jinja(app): + loader = jinja2.FileSystemLoader(str(TEMPLATES_ROOT)) + aiohttp_jinja2.setup(app, loader=loader) + + +async def start(): + await client.start() + + +async def stop(app): + await client.disconnect() + + +async def init(): + server = web.Application() + await start() + setup_routes(server, Views(client)) + setup_jinja(server) + server.on_cleanup.append(stop) + return server + + +def main(): + loop = asyncio.get_event_loop() + app = loop.run_until_complete(init()) + web.run_app(app, host=host, port=port) diff --git a/app/routes.py b/app/routes.py new file mode 100644 index 0000000..58e3d62 --- /dev/null +++ b/app/routes.py @@ -0,0 +1,15 @@ +from aiohttp import web + + +def setup_routes(app, handler): + h = handler + app.add_routes( + [ + web.get('/', h.index, name='index'), + web.get(r"/{id:\d+}/view", h.info, name='info'), + web.get(r"/{id:\d+}/download", h.download_get), + web.head(r"/{id:\d+}/download", h.download_head), + web.get(r"/{id:\d+}/thumbnail", h.thumbnail_get), + web.head(r"/{id:\d+}/thumbnail", h.thumbnail_head), + ] + ) diff --git a/app/telegram.py b/app/telegram.py new file mode 100644 index 0000000..a3cf3d8 --- /dev/null +++ b/app/telegram.py @@ -0,0 +1,37 @@ +import math +import logging +import asyncio + +from telethon import TelegramClient +from telethon.sessions import StringSession + +class Client(TelegramClient): + + def __init__(self, session_string, *args, **kwargs): + super().__init__(StringSession(session_string), *args, **kwargs) + self.log = logging.getLogger(__name__) + + async def download(self, file, file_size, offset, limit): + part_size = 1024 * 1024 + first_part_cut = offset % part_size + first_part = math.floor(offset / part_size) + last_part_cut = part_size - (limit % part_size) + last_part = math.ceil(limit / part_size) + part_count = math.ceil(file_size / part_size) + part = first_part + try: + async for chunk in self.iter_download(file, offset=first_part * part_size, file_size=file_size, limit=part_size): + if part == first_part: + yield chunk[first_part_cut:] + elif part == last_part: + yield chunk[:last_part_cut] + else: + yield chunk + self.log.debug(f"Part {part}/{last_part} (total {part_count}) downloaded") + part += 1 + self.log.debug("download finished") + except (GeneratorExit, StopAsyncIteration, asyncio.CancelledError): + self.log.debug("download interrupted") + raise + except Exception: + self.log.debug("download errored", exc_info=True) \ No newline at end of file diff --git a/app/templates/footer.html b/app/templates/footer.html new file mode 100644 index 0000000..44d75cb --- /dev/null +++ b/app/templates/footer.html @@ -0,0 +1,8 @@ + + + + + + diff --git a/app/templates/header.html b/app/templates/header.html new file mode 100644 index 0000000..d9974da --- /dev/null +++ b/app/templates/header.html @@ -0,0 +1,20 @@ + + + + + + + + + + {{title}} + + + + + +
+ +
+

Telegram Index

+
\ No newline at end of file diff --git a/app/templates/index.html b/app/templates/index.html new file mode 100644 index 0000000..b287afc --- /dev/null +++ b/app/templates/index.html @@ -0,0 +1,46 @@ +{% include 'header.html' %} +
+ + +
+ +
+ + + + + + + + + + + + {% for item in item_list %} + + + + + + + + + {% endfor %} + +
FileIdMediaTypeDescriptionDateSize
{{item.file_id}}{{item.media}}{{item.mime_type}}{{item.insight}}{{item.date}}{{item.size}}
+ +
+ + + +
+ {% if prev_page %} + Page {{prev_page.no}} + {% endif %} +

Page {{cur_page}}

+ {% if next_page %} + Page {{next_page.no}} + {% endif %} +
+ +{% include 'footer.html' %} \ No newline at end of file diff --git a/app/templates/info.html b/app/templates/info.html new file mode 100644 index 0000000..581c0dd --- /dev/null +++ b/app/templates/info.html @@ -0,0 +1,91 @@ +{% include 'header.html' %} + +
+ {% if found %} + {% if media %} +

+ Download {{name}} +

+ +
+ {% if media.image %} + {{name}} + {% elif media.video %} + + {% elif media.audio %} + + {% endif %} + + {% if caption %} + +
+

{{ caption|safe }}

+
+ + {% endif %} + +
+ + + {% else %} +
+

{{ text|safe }}

+
+ {% endif %} + + {% else %} +

Ooops...

+ +

+ {{ reason }} +

+ {% endif %} + +
+ + + +{% include 'footer.html' %} \ No newline at end of file diff --git a/app/util.py b/app/util.py new file mode 100644 index 0000000..7be54e4 --- /dev/null +++ b/app/util.py @@ -0,0 +1,14 @@ +def get_file_name(message): + if message.file.name: + return message.file.name.replace('\n', ' ') + ext = message.file.ext or "" + return f"{message.date.strftime('%Y-%m-%d_%H:%M:%S')}{ext}" + + +def get_human_size(num): + base = 1024.0 + sufix_list = ['B','KiB','MiB','GiB','TiB','PiB','EiB','ZiB', 'YiB'] + for unit in sufix_list: + if abs(num) < base: + return f"{round(num, 2)} {unit}" + num /= base \ No newline at end of file diff --git a/app/views.py b/app/views.py new file mode 100644 index 0000000..631490e --- /dev/null +++ b/app/views.py @@ -0,0 +1,216 @@ +from aiohttp import web +import aiohttp_jinja2 +from jinja2 import Markup +from telethon.tl import types +from telethon.tl.custom import Message + +from .util import get_file_name, get_human_size +from .config import chat_id + + +class Views: + + def __init__(self, client): + self.client = client + + + @aiohttp_jinja2.template('index.html') + async def index(self, req): + try: + offset_val = int(req.query.get('page', '1')) + except: + offset_val = 1 + try: + search_query = req.query.get('search', '') + except: + search_query = '' + offset_val = 0 if offset_val <=1 else offset_val-1 + try: + if search_query: + messages = (await self.client.get_messages(chat_id, search=search_query, limit=20, add_offset=20*offset_val)) or [] + else: + messages = (await self.client.get_messages(chat_id, limit=20, add_offset=20*offset_val)) or [] + except: + messages = [] + results = [] + for m in messages: + if m.file and not isinstance(m.media, types.MessageMediaWebPage): + entry = dict( + file_id=m.id, + media=True, + mime_type=m.file.mime_type, + insight = get_file_name(m)[:55], + date = m.date.isoformat(), + size=get_human_size(m.file.size) + ) + elif m.message: + entry = dict( + file_id=m.id, + media=False, + mime_type='text/plain', + insight = m.raw_text[:55], + date = m.date.isoformat(), + size=get_human_size(len(m.raw_text)) + ) + results.append(entry) + prev_page = False + next_page = False + if offset_val: + query = {'page':offset_val} + if search_query: + query.update({'search':search_query}) + prev_page = { + 'url': req.rel_url.with_query(query), + 'no': offset_val + } + + if len(messages)==20: + query = {'page':offset_val+2} + if search_query: + query.update({'search':search_query}) + next_page = { + 'url': req.rel_url.with_query(query), + 'no': offset_val+2 + } + + return { + 'item_list':results, + 'prev_page': prev_page, + 'cur_page' : offset_val+1, + 'next_page': next_page, + 'search': search_query, + 'title': "Telegram Index" + } + + + @aiohttp_jinja2.template('info.html') + async def info(self, req): + file_id = int(req.match_info["id"]) + message = await self.client.get_messages(entity=chat_id, ids=file_id) + if not message or not isinstance(message, Message): + print(type(message)) + return { + 'found':False, + 'reason' : "Entry you are looking for cannot be retrived!", + 'title': "Telegram Index" + } + if message.file and not isinstance(message.media, types.MessageMediaWebPage): + file_name = get_file_name(message) + file_size = get_human_size(message.file.size) + media = { + 'type':message.file.mime_type + } + if 'video/' in message.file.mime_type: + media.update({ + 'video' : True + }) + elif 'audio/' in message.file.mime_type: + media['audio'] = True + elif 'image/' in message.file.mime_type: + media['image'] = True + + if message.text: + caption = Markup.escape(message.raw_text).__str__().replace('\n', '
') + + else: + caption = False + return { + 'found': True, + 'name': file_name, + 'id': file_id, + 'size': file_size, + 'media': media, + 'caption': caption, + 'title': f"Download | {file_name} | {file_size}" + } + elif message.message: + text = Markup.escape(message.raw_text).__str__().replace('\n', '
') + return { + 'found': True, + 'media': False, + 'text': text, + 'title': "Telegram Index" + } + else: + return { + 'found':False, + 'reason' : "Some kind of entry that I cannot display", + 'title': "Telegram Index" + } + + + async def download_get(self, req): + return await self.handle_request(req) + + + async def download_head(self, req): + return await self.handle_request(req, head=True) + + + async def thumbnail_get(self, req): + return await self.handle_request(req, thumb=True) + + + async def thumbnail_head(self, req): + return await self.handle_request(req, head=True, thumb=True) + + + async def handle_request(self, req, head=False, thumb=False): + file_id = int(req.match_info["id"]) + + message = await self.client.get_messages(entity=chat_id, ids=file_id) + if not message or not message.file: + return web.Response(status=410, text="410: Gone. Access to the target resource is no longer available!") + + if thumb and message.document: + thumbnail = message.document.thumbs + if not thumbnail: + return web.Response(status=404, text="404: Not Found") + thumbnail = thumbnail[-1] + mime_type = 'image/jpeg' + size = thumbnail.size + file_name = f"{file_id}_thumbnail.jpg" + media = types.InputDocumentFileLocation( + id=message.document.id, + access_hash=message.document.access_hash, + file_reference=message.document.file_reference, + thumb_size=thumbnail.type + ) + else: + media = message.media + size = message.file.size + file_name = get_file_name(message) + mime_type = message.file.mime_type + + try: + offset = req.http_range.start or 0 + limit = req.http_range.stop or size + if (limit > size) or (offset < 0) or (limit < offset): + raise ValueError("range not in acceptable format") + except ValueError: + return web.Response( + status=416, + text="416: Range Not Satisfiable", + headers = { + "Content-Range": f"bytes */{size}" + } + ) + + if not head: + body = self.client.download(media, size, offset, limit) + else: + body = None + + headers = { + "Content-Type": mime_type, + "Content-Range": f"bytes {offset}-{limit}/{size}", + "Content-Length": str(limit - offset), + "Accept-Ranges": "bytes", + "Content-Disposition": f'attachment; filename="{file_name}"' + } + + return web.Response( + status=206 if offset else 200, + body=body, + headers=headers + ) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..cd8c081 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,4 @@ +aiohttp +aiohttp-jinja2 +telethon +cryptg diff --git a/runtime.txt b/runtime.txt new file mode 100644 index 0000000..ef7ef12 --- /dev/null +++ b/runtime.txt @@ -0,0 +1 @@ +python-3.8.5