Merge pull request #21 from odysseusmax/develop

Develop
This commit is contained in:
Christy Roys
2020-12-12 22:03:49 +05:30
committed by GitHub
17 changed files with 563 additions and 587 deletions

1
.gitignore vendored
View File

@@ -3,3 +3,4 @@
__pycache__/ __pycache__/
venv/ venv/
*.sh *.sh
logo/

View File

@@ -13,79 +13,73 @@
## Demo ## Demo
Hosted demo site: https://tg-index-demo.herokuapp.com Hosted demo site: <https://tg-index-demo.herokuapp.com>
## Deploy Guide ## Deploy Guide
* **Clone to local machine.** * **Clone to local machine.**
```bash ``` bash
$ git clone https://github.com/odysseusmax/tg-index.git git clone https://github.com/odysseusmax/tg-index.git
$ cd tg-index cd tg-index
``` ```
* **Create and activate virtual environment.** * **Create and activate virtual environment.**
```bash ``` bash
$ pip3 install virtualenv pip3 install virtualenv
$ virtualenv venv virtualenv venv
$ source venv/bin/activate source venv/bin/activate
``` ```
* **Install dependencies.** * **Install dependencies.**
```bash ``` bash
$ pip3 install -U -r requirements.txt pip3 install -U -r requirements.txt
``` ```
* **Environment Variables.** * **Environment Variables.**
| Variable Name | Value | Variable Name | Value
|------------- | ------------- |------------- | -------------
| `API_ID` (required) | Telegram api_id obtained from https://my.telegram.org/apps. | `API_ID` (required) | Telegram api_id obtained from <https://my.telegram.org/apps>.
| `API_HASH` (required) | Telegram api_hash obtained from https://my.telegram.org/apps. | `API_HASH` (required) | Telegram api_hash obtained from <https://my.telegram.org/apps>.
| `INDEX_SETTINGS` (required) | See the below description. | `INDEX_SETTINGS` (required) | See the below description.
| `SESSION_STRING` (required) | String obtained by running `$ python3 app/generate_session_string.py`. (Login with the telegram account which is a participant of the given channel (or chat). | `SESSION_STRING` (required) | String obtained by running `$ python3 app/generate_session_string.py`. (Login with the telegram account which is a participant of the given channel (or chat).
| `PORT` (optional) | Port on which app should listen to, defaults to 8080. | `PORT` (optional) | Port on which app should listen to, defaults to 8080.
| `HOST` (optional) | Host name on which app should listen to, defaults to 0.0.0.0. | `HOST` (optional) | Host name on which app should listen to, defaults to 0.0.0.0.
| `DEBUG` (optional) | Give some value to set logging level to debug, info by default. | `DEBUG` (optional) | Give some value to set logging level to debug, info by default.
| `BLOCK_DOWNLOADS` (optional) | Enable downloads or not. If provided, downloads will be disabled.
| `RESULTS_PER_PAGE` (optional) | Number of results to be returned per page defaults to 20.
* **Setting value for `INDEX_SETTINGS`** * **Setting value for `INDEX_SETTINGS`**
This is the general format, change the values of corresponding fields as your requirements. Remember to remove spaces. This is the general format, change the values of corresponding fields as your requirements. Remember to remove spaces.
``` ``` json
{ {
"index_all": true, "index_all": true,
"index_private":false, "index_private":false,
"index_group": false, "index_group": false,
"index_channel": true, "index_channel": true,
"exclude_chats": [], "exclude_chats": [],
"include_chats": [], "include_chats": []
"otg": {
"enable": false,
"include_private": false,
"include_group": true,
"include_channel": true
}
} }
``` ```
>
> * `index_all` - Whether to consider all the chats associated with the telegram account. Value should either be `true` or `false`. > * `index_all` - Whether to consider all the chats associated with the telegram account. Value should either be `true` or `false`.
> * `index_private` - Whether to index private chats. Only considered if `index_all` is set to `true`. Value should either be `true` or `false`. > * `index_private` - Whether to index private chats. Only considered if `index_all` is set to `true`. Value should either be `true` or `false`.
> * `index_group` - Whether to index group chats. Only considered if `index_all` is set to `true`. Value should either be `true` or `false`. > * `index_group` - Whether to index group chats. Only considered if `index_all` is set to `true`. Value should either be `true` or `false`.
> * `index_channel` - Whether to index channels. Only considered if `index_all` is set to `true`. Value should either be `true` or `false`. > * `index_channel` - Whether to index channels. Only considered if `index_all` is set to `true`. Value should either be `true` or `false`.
> * `exclude_chats` - An array/list of chat id's that should be ignored for indexing. Only considered if `index_all` is set to `true`. > * `exclude_chats` - An array/list of chat id's that should be ignored for indexing. Only considered if `index_all` is set to `true`.
> * `include_chats` - An array/list of chat id's to index. Only considered if `index_all` is set to `false`. > * `include_chats` - An array/list of chat id's to index. Only considered if `index_all` is set to `false`.
> * `otg` - On-The-Go Indexing settings. Whether to allow indexing channels/chats other than the specified chats dynamically on the go.
* **Run app.** * **Run app.**
```bash ``` bash
$ python3 -m app python3 -m app
``` ```
## API ## API
Here's the api description. [API](https://github.com/odysseusmax/tg-index/wiki/API) Here's the api description. [API](https://github.com/odysseusmax/tg-index/wiki/API)
@@ -99,4 +93,5 @@ Contributions are welcome.
You can contact me [@odysseusmax](https://tx.me/odysseusmax). You can contact me [@odysseusmax](https://tx.me/odysseusmax).
## License ## License
Code released under [The GNU General Public License](LICENSE). Code released under [The GNU General Public License](LICENSE).

View File

@@ -1,3 +1,4 @@
from pathlib import Path
import traceback import traceback
import json import json
import sys import sys
@@ -24,8 +25,6 @@ except (KeyError, ValueError):
try: try:
index_settings_str = os.environ["INDEX_SETTINGS"].strip() index_settings_str = os.environ["INDEX_SETTINGS"].strip()
index_settings = json.loads(index_settings_str) index_settings = json.loads(index_settings_str)
otg_settings = index_settings['otg']
enable_otg = otg_settings['enable']
except: except:
traceback.print_exc() traceback.print_exc()
print("\n\nPlease set the INDEX_SETTINGS environment variable correctly") print("\n\nPlease set the INDEX_SETTINGS environment variable correctly")
@@ -40,5 +39,7 @@ except (KeyError, ValueError):
host = os.environ.get("HOST", "0.0.0.0") host = os.environ.get("HOST", "0.0.0.0")
debug = bool(os.environ.get("DEBUG")) debug = bool(os.environ.get("DEBUG"))
chat_ids = [] block_downloads = bool(os.environ.get("BLOCK_DOWNLOADS"))
alias_ids = [] results_per_page = int(os.environ.get("RESULTS_PER_PAGE", "20"))
logo_folder = Path('logo/')
logo_folder.mkdir(exist_ok=True)

View File

@@ -1,8 +1,10 @@
import os
from telethon.sync import TelegramClient from telethon.sync import TelegramClient
from telethon.sessions import StringSession from telethon.sessions import StringSession
api_id = int(input("Enter your API_ID: ")) api_id = int(os.environ.get('API_ID') or input("Enter your API_ID: "))
api_hash = input("Enter your API_HASH: ") api_hash = os.environ.get('API_HASH') or input("Enter your API_HASH: ")
with TelegramClient(StringSession(), api_id, api_hash) as client: with TelegramClient(StringSession(), api_id, api_hash) as client:
print(client.session.save()) print(client.session.save())

View File

@@ -3,75 +3,70 @@ import string
import logging import logging
from aiohttp import web from aiohttp import web
from telethon.tl.types import Channel, Chat, User
from .config import index_settings, alias_ids, chat_ids from .config import index_settings
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
def generate_alias_id(chat):
chat_id = chat.id
title = chat.title
while True:
alias_id = ''.join([random.choice(string.ascii_letters + string.digits) for _ in range(len(str(chat_id)))])
if alias_id in alias_ids:
continue
alias_ids.append(alias_id)
chat_ids.append({
'chat_id': chat_id,
'alias_id': alias_id,
'title': title
})
return alias_id
async def setup_routes(app, handler): async def setup_routes(app, handler):
h = handler h = handler
client = h.client client = h.client
p = r"/{chat:[^/]+}"
routes = [
web.get('/', h.home),
web.post('/otg', h.dynamic_view),
web.get('/otg', h.otg_view),
web.get(p, h.index),
web.get(p + r"/logo", h.logo),
web.get(p + r"/{id:\d+}/view", h.info),
web.get(p + r"/{id:\d+}/download", h.download_get),
web.head(p + r"/{id:\d+}/download", h.download_head),
web.get(p + r"/{id:\d+}/thumbnail", h.thumbnail_get),
web.view(r'/{wildcard:.*}', h.wildcard)
]
index_all = index_settings['index_all'] index_all = index_settings['index_all']
index_private = index_settings['index_private'] index_private = index_settings['index_private']
index_group = index_settings['index_group'] index_group = index_settings['index_group']
index_channel = index_settings['index_channel'] index_channel = index_settings['index_channel']
exclude_chats = index_settings['exclude_chats'] exclude_chats = index_settings['exclude_chats']
include_chats = index_settings['include_chats'] include_chats = index_settings['include_chats']
routes = [
web.get('/', h.home)
]
if index_all: if index_all:
#print(await client.get_dialogs())
async for chat in client.iter_dialogs(): async for chat in client.iter_dialogs():
alias_id = None alias_id = None
if chat.id in exclude_chats: if chat.id in exclude_chats:
continue continue
if chat.is_user: entity = chat.entity
if index_private:
alias_id = generate_alias_id(chat)
elif chat.is_channel:
if index_channel:
alias_id = generate_alias_id(chat)
else:
if index_group:
alias_id = generate_alias_id(chat)
if not alias_id: if isinstance(entity, User) and not index_private:
print(f'{chat.title}, private: {index_private}')
continue continue
log.debug(f"Index added for {chat.id} :: {chat.title} at /{alias_id}") elif isinstance(entity, Channel) and not index_channel:
print(f'{chat.title}, channel: {index_channel}')
continue
elif isinstance(entity, Chat) and not index_group:
print(f'{chat.title}, group: {index_group}')
continue
alias_id = h.generate_alias_id(chat)
p = "/{chat:" + alias_id + "}"
routes.extend([
web.get(p, h.index),
web.get(p + r"/logo", h.logo),
web.get(p + r"/{id:\d+}/view", h.info),
web.get(p + r"/{id:\d+}/download", h.download_get),
web.head(p + r"/{id:\d+}/download", h.download_head),
web.get(p + r"/{id:\d+}/thumbnail", h.thumbnail_get),
])
log.debug(f"Index added for {chat.id} at /{alias_id}")
else: else:
for chat_id in include_chats: for chat_id in include_chats:
chat = await client.get_entity(chat_id) chat = await client.get_entity(chat_id)
alias_id = generate_alias_id(chat) alias_id = h.generate_alias_id(chat)
log.debug(f"Index added for {chat.id} :: {chat.title} at /{alias_id}") p = "/{chat:" + alias_id + "}"
routes.extend([
web.get(p, h.index),
web.get(p + r"/logo", h.logo),
web.get(p + r"/{id:\d+}/view", h.info),
web.get(p + r"/{id:\d+}/download", h.download_get),
web.head(p + r"/{id:\d+}/download", h.download_head),
web.get(p + r"/{id:\d+}/thumbnail", h.thumbnail_get),
])
log.debug(f"Index added for {chat.id} at /{alias_id}")
routes.append(web.view(r'/{wildcard:.*}', h.wildcard))
app.add_routes(routes) app.add_routes(routes)

View File

@@ -10,7 +10,7 @@ class Client(TelegramClient):
def __init__(self, session_string, *args, **kwargs): def __init__(self, session_string, *args, **kwargs):
super().__init__(StringSession(session_string), *args, **kwargs) super().__init__(StringSession(session_string), *args, **kwargs)
self.log = logging.getLogger(__name__) self.log = logging.getLogger(__name__)
async def download(self, file, file_size, offset, limit): async def download(self, file, file_size, offset, limit):
part_size_kb = utils.get_appropriated_part_size(file_size) part_size_kb = utils.get_appropriated_part_size(file_size)
@@ -25,7 +25,7 @@ class Client(TelegramClient):
async for chunk in self.iter_download(file, offset=first_part * part_size, request_size=part_size): async for chunk in self.iter_download(file, offset=first_part * part_size, request_size=part_size):
if part == first_part: if part == first_part:
yield chunk[first_part_cut:] yield chunk[first_part_cut:]
elif part == last_part: elif part == last_part-1:
yield chunk[:last_part_cut] yield chunk[:last_part_cut]
else: else:
yield chunk yield chunk

View File

@@ -17,7 +17,4 @@
<header class="flex justify-between bg-red-600 text-white mb-2 p-4 w-full sticky top-0 shadow"> <header class="flex justify-between bg-red-600 text-white mb-2 p-4 w-full sticky top-0 shadow">
<a href="/" class="text-left text-xl lg:text-2xl xl:text-3xl"> Telegram Index </a> <a href="/" class="text-left text-xl lg:text-2xl xl:text-3xl"> Telegram Index </a>
{% if otg %}
<a title="On-The-Go Indexing" href="/otg" class="text-xl lg:text-2xl xl:text-3xl"> OTG Indexing </a>
{% endif %}
</header> </header>

View File

@@ -1,26 +1,26 @@
{% include 'header.html' %} {% include 'header.html' %}
<div class="p-4 space-y-8"> <div class="p-4 space-y-8">
{% if found %} {% if found %}
{% if media %} {% if media %}
<h1 class="text-blue-500 text-center text-lg lg:text-xl xl:text-3xl w-full break-all"> <h1 class="text-blue-500 text-center text-lg lg:text-xl xl:text-3xl w-full break-all">
Download {{name}} Download {{name}}
</h1> </h1>
<div class="mx-auto w-full md:w-3/4 lg:w-2/5 p-2"> <div class="mx-auto w-full p-2 md:w-3/4 lg:w-2/5 p-2">
{% if media.image %} {% if media.image %}
<img class="mx-auto rounded" src="../{{file_id}}/thumbnail" alt="{{name}}"> <img class="mx-auto rounded" src="{{thumbnail}}" alt="{{name}}">
{% elif media.video %} {% elif media.video %}
<div id="video-warning" class="mx-auto p-4 bg-gray-600 text-gray-300 rounded border text-center hidden break-words"> <div id="video-warning" class="mx-auto p-4 bg-gray-600 text-gray-300 rounded border text-center hidden break-words">
<p> Video {{name}} could not be played!</p> <p> Video {{name}} could not be played!</p>
</div> </div>
<div id="my-video-wrapper"> <div id="my-video-wrapper">
<video id="my-video-player" class="mx-auto rounded" controls poster="../{{file_id}}/thumbnail"> <video id="my-video-player" class="mx-auto rounded" controls poster="{{thumbnail}}">
<source src="../{{file_id}}/download" type="video/mp4" /> <source src="{{download_url}}" type="video/mp4" />
</video> </video>
</div> </div>
<script> <script>
var video = document.querySelector("video"); var video = document.querySelector("video");
var src = video.firstElementChild var src = video.firstElementChild
@@ -29,7 +29,7 @@
console.log(evt); console.log(evt);
document.getElementById('my-video-wrapper').style.display = 'none'; document.getElementById('my-video-wrapper').style.display = 'none';
document.getElementById('video-warning').style.display = 'block'; document.getElementById('video-warning').style.display = 'block';
}); });
var myFP = fluidPlayer( var myFP = fluidPlayer(
'my-video-player',{ 'my-video-player',{
"layoutControls": { "layoutControls": {
@@ -50,11 +50,11 @@
<div id="audio-warning" class="mx-auto p-4 bg-gray-600 text-gray-300 rounded border text-center hidden break-words"> <div id="audio-warning" class="mx-auto p-4 bg-gray-600 text-gray-300 rounded border text-center hidden break-words">
<p> Audio {{name}} could not be played!</p> <p> Audio {{name}} could not be played!</p>
</div> </div>
<div id="my-audio-wrapper"> <div id="my-audio-wrapper">
<img class="mx-auto rounded w-full" src="../{{file_id}}/thumbnail" alt="{{name}}"> <img class="mx-auto rounded w-full" src="{{thumbnail}}" alt="{{name}}">
<audio class="mx-auto" controls muted> <audio class="mx-auto" controls muted>
<source src="../{{file_id}}/download" type="audio/mpeg" /> <source src="{{download_url}}" type="audio/mpeg" />
</audio> </audio>
</div> </div>
<script> <script>
@@ -68,9 +68,9 @@
}); });
</script> </script>
{% endif %} {% endif %}
{% if caption_html %} {% if caption_html %}
<div class="mx-auto mt-1"> <div class="mx-auto mt-1">
<div class="flex justify-center text-gray-300 font-mono"> <div class="flex justify-center text-gray-300 font-mono">
<p class="text-left w-full rounded p-4 bg-gray-900 shadow-lg"> {{ caption_html|safe }} </p> <p class="text-left w-full rounded p-4 bg-gray-900 shadow-lg"> {{ caption_html|safe }} </p>
@@ -87,15 +87,15 @@
</div> </div>
{% endif %} {% endif %}
</div> </div>
{% endif %} {% endif %}
</div> </div>
<div class="text-center text-white"> <div class="text-center text-white">
<a class="rounded p-3 bg-indigo-500 hover:bg-indigo-700 shadow-lg" href="../{{file_id}}/download">Download Now! ({{ human_size }})</a> <a class="rounded p-3 bg-indigo-500 hover:bg-indigo-700 shadow-lg{% if block_downloads %} cursor-not-allowed disabled{% endif %}" download="{{name}}" href="{{download_url}}">Download Now! ({{ human_size }})</a>
</div> </div>
{% else %} {% else %}
<div class="mx-auto flex flex-wrap justify-center w-full md:w-3/4 mt-1"> <div class="mx-auto flex flex-wrap justify-center w-full md:w-3/4 mt-1">
@@ -115,15 +115,15 @@
{% endif %} {% endif %}
</div> </div>
{% endif %} {% endif %}
{% else %} {% else %}
<h1 class="text-blue-500 text-center text-2xl md:text-3xl lg:text-4xl xl:text-5xl">Ooops...</h1> <h1 class="text-blue-500 text-center text-2xl md:text-3xl lg:text-4xl xl:text-5xl">Ooops...</h1>
<p class="text-center text-sm md:text-base lg:text-xl xl:text-2xl font-semibold"> <p class="text-center text-sm md:text-base lg:text-xl xl:text-2xl font-semibold">
{{ reason }} {{ reason }}
</p> </p>
{% endif %} {% endif %}
</div> </div>
{% include 'footer.html' %} {% include 'footer.html' %}

View File

@@ -1,483 +0,0 @@
import logging
from PIL import Image, ImageDraw, ImageFont
import random
import io
from aiohttp import web
import aiohttp_jinja2
from jinja2 import Markup
from telethon.tl import types
from telethon.tl.custom import Message
from telethon.tl.types import User, Chat, Channel
from .util import get_file_name, get_human_size
from .config import otg_settings, chat_ids, enable_otg
log = logging.getLogger(__name__)
class Views:
def __init__(self, client):
self.client = client
async def wildcard(self, req):
raise web.HTTPFound('/')
@aiohttp_jinja2.template('home.html')
async def home(self, req):
if len(chat_ids) == 1:
raise web.HTTPFound(f"{chat_ids[0]['alias_id']}")
chats = []
for chat in chat_ids:
chats.append({
'page_id': chat['alias_id'],
'name': chat['title'],
'url': req.rel_url.path + f"/{chat['alias_id']}"
})
return {'chats':chats, 'otg': enable_otg}
@aiohttp_jinja2.template('otg.html')
async def otg_view(self, req):
if not enable_otg:
raise web.HTTPFound('/')
return_data = {}
error = req.query.get('e')
if error:
return_data.update({'error': error})
return return_data
async def dynamic_view(self, req):
if not enable_otg:
raise web.HTTPFound('/')
rel_url = req.rel_url
include_private = otg_settings['include_private']
include_group = otg_settings['include_group']
include_channel = otg_settings['include_channel']
post_data = await req.post()
raw_id = post_data.get('id')
if not raw_id:
raise web.HTTPFound('/')
raw_id.replace('@', '')
try:
chat = await self.client.get_entity(raw_id)
except Exception as e:
log.debug(e, exc_info=True)
raise web.HTTPFound(rel_url.with_query({'e': f"No chat found with username {raw_id}"}))
if isinstance(chat, User) and not include_private:
raise web.HTTPFound(rel_url.with_query({'e': "Indexing private chats is not supported!!"}))
elif isinstance(chat, Channel) and not include_channel:
raise web.HTTPFound(rel_url.with_query({'e': "Indexing channels is not supported!!"}))
elif isinstance(chat, Chat) and not include_group:
raise web.HTTPFound(rel_url.with_query({'e': "Indexing group chats is not supported!!"}))
log.debug(f"chat {chat} accessed!!")
raise web.HTTPFound(f'/{chat.id}')
@aiohttp_jinja2.template('index.html')
async def index(self, req):
alias_id = req.match_info['chat']
chat = [i for i in chat_ids if i['alias_id'] == alias_id]
if not chat:
if not enable_otg:
raise web.HTTPFound('/')
try:
chat_id = int(alias_id)
chat_ = await self.client.get_entity(chat_id)
chat_name = chat_.title
except:
raise web.HTTPFound('/')
else:
chat = chat[0]
chat_id = chat['chat_id']
chat_name = chat['title']
log_msg = ''
try:
offset_val = int(req.query.get('page', '1'))
except:
offset_val = 1
log_msg += f"page: {offset_val} | "
try:
search_query = req.query.get('search', '')
except:
search_query = ''
log_msg += f"search query: {search_query} | "
offset_val = 0 if offset_val <=1 else offset_val-1
try:
kwargs = {
'entity': chat_id,
'limit': 20,
'add_offset': 20*offset_val
}
if search_query:
kwargs.update({'search': search_query})
messages = (await self.client.get_messages(**kwargs)) or []
except:
log.debug("failed to get messages", exc_info=True)
messages = []
log_msg += f"found {len(messages)} results | "
log.debug(log_msg)
results = []
for m in messages:
entry = None
if m.file and not isinstance(m.media, types.MessageMediaWebPage):
entry = dict(
file_id=m.id,
media=True,
thumbnail=f"/{alias_id}/{m.id}/thumbnail",
mime_type=m.file.mime_type,
insight = get_file_name(m),
date = str(m.date),
size=m.file.size,
human_size=get_human_size(m.file.size),
url=req.rel_url.path + f"/{m.id}/view"
)
elif m.message:
entry = dict(
file_id=m.id,
media=False,
mime_type='text/plain',
insight = m.raw_text[:100],
date = str(m.date),
size=len(m.raw_text),
human_size=get_human_size(len(m.raw_text)),
url=req.rel_url.path + f"/{m.id}/view"
)
if entry:
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': str(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': str(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,
'name' : chat_name,
'logo': f"/{alias_id}/logo",
'title' : "Index of " + chat_name
}
@aiohttp_jinja2.template('info.html')
async def info(self, req):
file_id = int(req.match_info["id"])
alias_id = req.match_info['chat']
chat = [i for i in chat_ids if i['alias_id'] == alias_id]
if not chat:
if not enable_otg:
raise web.HTTPFound('/')
try:
chat_id = int(alias_id)
except:
raise web.HTTPFound('/')
else:
chat = chat[0]
chat_id = chat['chat_id']
try:
message = await self.client.get_messages(entity=chat_id, ids=file_id)
except:
log.debug(f"Error in getting message {file_id} in {chat_id}", exc_info=True)
message = None
if not message or not isinstance(message, Message):
log.debug(f"no valid entry for {file_id} in {chat_id}")
return {
'found':False,
'reason' : "Entry you are looking for cannot be retrived!",
}
return_val = {}
reply_btns = []
if message.reply_markup:
if isinstance(message.reply_markup, types.ReplyInlineMarkup):
for button_row in message.reply_markup.rows:
btns = []
for button in button_row.buttons:
if isinstance(button, types.KeyboardButtonUrl):
btns.append({'url': button.url, 'text': button.text})
reply_btns.append(btns)
if message.file and not isinstance(message.media, types.MessageMediaWebPage):
file_name = get_file_name(message)
file_size = message.file.size
human_file_size = get_human_size(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 = message.raw_text
else:
caption = ''
caption_html = Markup.escape(caption).__str__().replace('\n', '<br>')
return_val = {
'found': True,
'name': file_name,
'file_id': file_id,
'size': file_size,
'human_size': human_file_size,
'media': media,
'caption_html': caption_html,
'caption': caption,
'title': f"Download | {file_name} | {human_file_size}",
'reply_btns': reply_btns,
'thumbnail': f"/{alias_id}/{file_id}/thumbnail",
'download_url': f"/{alias_id}/{file_id}/download",
'page_id': alias_id
}
elif message.message:
text = message.raw_text
text_html = Markup.escape(text).__str__().replace('\n', '<br>')
return_val = {
'found': True,
'media': False,
'text': text,
'text_html': text_html,
'reply_btns': reply_btns,
'page_id': alias_id
}
else:
return_val = {
'found':False,
'reason' : "Some kind of entry that I cannot display",
}
log.debug(f"data for {file_id} in {chat_id} returned as {return_val}")
return return_val
async def logo(self, req):
alias_id = req.match_info['chat']
chat = [i for i in chat_ids if i['alias_id'] == alias_id]
if not chat:
if not enable_otg:
return web.Response(status=403, text="403: Forbiden")
try:
chat_id = int(alias_id)
except:
return web.Response(status=403, text="403: Forbiden")
else:
chat = chat[0]
chat_id = chat['chat_id']
chat_name = "Image not available"
try:
photo = await self.client.get_profile_photos(chat_id)
except:
log.debug(f"Error in getting profile picture in {chat_id}", exc_info=True)
photo = None
if not photo:
W, H = (160, 160)
c = lambda : random.randint(0, 255)
color = tuple([c() for i in range(3)])
im = Image.new("RGB", (W,H), color)
draw = ImageDraw.Draw(im)
w, h = draw.textsize(chat_name)
draw.text(((W-w)/2,(H-h)/2), chat_name, fill="white")
temp = io.BytesIO()
im.save(temp, "PNG")
body = temp.getvalue()
else:
photo = photo[0]
pos = -1 if req.query.get('big', None) else int(len(photo.sizes)/2)
size = self.client._get_thumb(photo.sizes, pos)
if isinstance(size, (types.PhotoCachedSize, types.PhotoStrippedSize)):
body = self.client._download_cached_photo_size(size, bytes)
else:
media = types.InputPhotoFileLocation(
id=photo.id,
access_hash=photo.access_hash,
file_reference=photo.file_reference,
thumb_size=size.type
)
body = self.client.iter_download(media)
r = web.Response(
status=200,
body=body,
headers={
"Content-Type": "image/jpeg",
"Content-Disposition": 'inline; filename="logo.jpg"'
}
)
#r.enable_chunked_encoding()
return r
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):
file_id = int(req.match_info["id"])
alias_id = req.match_info['chat']
chat = [i for i in chat_ids if i['alias_id'] == alias_id]
if not chat:
if not enable_otg:
return web.Response(status=403, text="403: Forbiden")
try:
chat_id = int(alias_id)
except:
return web.Response(status=403, text="403: Forbiden")
else:
chat = chat[0]
chat_id = chat['chat_id']
try:
message = await self.client.get_messages(entity=chat_id, ids=file_id)
except:
log.debug(f"Error in getting message {file_id} in {chat_id}", exc_info=True)
message = None
if not message or not message.file:
log.debug(f"no result for {file_id} in {chat_id}")
return web.Response(status=410, text="410: Gone. Access to the target resource is no longer available!")
if message.document:
media = message.document
thumbnails = media.thumbs
location = types.InputDocumentFileLocation
else:
media = message.photo
thumbnails = media.sizes
location = types.InputPhotoFileLocation
if not thumbnails:
c = lambda : random.randint(0, 255)
color = tuple([c() for i in range(3)])
im = Image.new("RGB", (100, 100), color)
temp = io.BytesIO()
im.save(temp, "PNG")
body = temp.getvalue()
else:
thumb_pos = int(len(thumbnails)/2)
thumbnail = self.client._get_thumb(thumbnails, thumb_pos)
if not thumbnail or isinstance(thumbnail, types.PhotoSizeEmpty):
return web.Response(status=410, text="410: Gone. Access to the target resource is no longer available!")
if isinstance(thumbnail, (types.PhotoCachedSize, types.PhotoStrippedSize)):
body = self.client._download_cached_photo_size(thumbnail, bytes)
else:
actual_file = location(
id=media.id,
access_hash=media.access_hash,
file_reference=media.file_reference,
thumb_size=thumbnail.type
)
body = self.client.iter_download(actual_file)
r = web.Response(
status=200,
body=body,
headers={
"Content-Type": "image/jpeg",
"Content-Disposition": 'inline; filename="thumbnail.jpg"'
}
)
#r.enable_chunked_encoding()
return r
async def handle_request(self, req, head=False):
file_id = int(req.match_info["id"])
alias_id = req.match_info['chat']
chat = [i for i in chat_ids if i['alias_id'] == alias_id]
if not chat:
if not enable_otg:
return web.Response(status=403, text="403: Forbiden")
try:
chat_id = int(alias_id)
except:
return web.Response(status=403, text="403: Forbiden")
else:
chat = chat[0]
chat_id = chat['chat_id']
try:
message = await self.client.get_messages(entity=chat_id, ids=file_id)
except:
log.debug(f"Error in getting message {file_id} in {chat_id}", exc_info=True)
message = None
if not message or not message.file:
log.debug(f"no result for {file_id} in {chat_id}")
return web.Response(status=410, text="410: Gone. Access to the target resource is no longer available!")
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)
log.info(f"Serving file in {message.id} (chat {chat_id}) ; Range: {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
)

39
app/views/__init__.py Normal file
View File

@@ -0,0 +1,39 @@
import random
import string
from telethon.utils import get_display_name
from .home_view import HomeView
from .wildcard_view import WildcardView
from .download import Download
from .index_view import IndexView
from .info_view import InfoView
from .logo_view import LogoView
from .thumbnail_view import ThumbnailView
class Views(HomeView, Download,
IndexView, InfoView,
LogoView, ThumbnailView,
WildcardView):
def __init__(self, client):
self.client = client
self.alias_ids = []
self.chat_ids = []
def generate_alias_id(self, chat):
chat_id = chat.id
title = chat.title
while True:
alias_id = ''.join([random.choice(string.ascii_letters + string.digits) for _ in range(len(str(chat_id)))])
if alias_id in self.alias_ids:
continue
self.alias_ids.append(alias_id)
self.chat_ids.append({
'chat_id': chat_id,
'alias_id': alias_id,
'title': title
})
return alias_id

78
app/views/download.py Normal file
View File

@@ -0,0 +1,78 @@
import logging
from aiohttp import web
from app.util import get_file_name
from app.config import block_downloads
log = logging.getLogger(__name__)
class Download:
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 handle_request(self, req, head=False):
if block_downloads:
return web.Response(status=403, text="403: Forbiden" if not head else None)
file_id = int(req.match_info["id"])
alias_id = req.match_info['chat']
chat = [i for i in self.chat_ids if i['alias_id'] == alias_id][0]
chat_id = chat['chat_id']
try:
message = await self.client.get_messages(entity=chat_id, ids=file_id)
except:
log.debug(f"Error in getting message {file_id} in {chat_id}", exc_info=True)
message = None
if not message or not message.file:
log.debug(f"no result for {file_id} in {chat_id}")
return web.Response(status=410, text="410: Gone. Access to the target resource is no longer available!" if not head else None)
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" if not head else None,
headers = {
"Content-Range": f"bytes */{size}"
}
)
if not head:
body = self.client.download(media, size, offset, limit)
log.info(f"Serving file in {message.id} (chat {chat_id}) ; Range: {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
)

19
app/views/home_view.py Normal file
View File

@@ -0,0 +1,19 @@
from aiohttp import web
import aiohttp_jinja2
class HomeView:
@aiohttp_jinja2.template('home.html')
async def home(self, req):
if len(self.chat_ids) == 1:
raise web.HTTPFound(f"{self.chat_ids[0]['alias_id']}")
chats = []
for chat in self.chat_ids:
chats.append({
'page_id': chat['alias_id'],
'name': chat['title'],
'url': f"/{chat['alias_id']}"
})
return {'chats': chats}

99
app/views/index_view.py Normal file
View File

@@ -0,0 +1,99 @@
import logging
import aiohttp_jinja2
from telethon.tl import types
from app.config import results_per_page
from app.util import get_file_name, get_human_size
log = logging.getLogger(__name__)
class IndexView:
@aiohttp_jinja2.template('index.html')
async def index(self, req):
alias_id = req.match_info['chat']
chat = [i for i in self.chat_ids if i['alias_id'] == alias_id][0]
log_msg = ''
try:
offset_val = int(req.query.get('page', '1'))
except:
offset_val = 1
log_msg += f"page: {offset_val} | "
try:
search_query = req.query.get('search', '')
except:
search_query = ''
log_msg += f"search query: {search_query} | "
offset_val = 0 if offset_val <=1 else offset_val-1
try:
kwargs = {
'entity': chat['chat_id'],
'limit': results_per_page,
'add_offset': results_per_page*offset_val
}
if search_query:
kwargs.update({'search': search_query})
messages = (await self.client.get_messages(**kwargs)) or []
except:
log.debug("failed to get messages", exc_info=True)
messages = []
log_msg += f"found {len(messages)} results | "
log.debug(log_msg)
results = []
for m in messages:
entry = None
if m.file and not isinstance(m.media, types.MessageMediaWebPage):
entry = dict(
file_id=m.id,
media=True,
thumbnail=f"/{alias_id}/{m.id}/thumbnail",
mime_type=m.file.mime_type,
insight = get_file_name(m),
human_size=get_human_size(m.file.size),
url=f"/{alias_id}/{m.id}/view"
)
elif m.message:
entry = dict(
file_id=m.id,
media=False,
mime_type='text/plain',
insight = m.raw_text[:100],
url=f"/{alias_id}/{m.id}/view"
)
if entry:
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': str(req.rel_url.with_query(query)),
'no': offset_val
}
if len(messages)==results_per_page:
query = {'page':offset_val+2}
if search_query:
query.update({'search':search_query})
next_page = {
'url': str(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,
'name' : chat['title'],
'logo': f"/{alias_id}/logo",
'title' : "Index of " + chat['title']
}

92
app/views/info_view.py Normal file
View File

@@ -0,0 +1,92 @@
import logging
import aiohttp_jinja2
from telethon.tl import types
from telethon.tl.custom import Message
from jinja2 import Markup
from app.util import get_file_name, get_human_size
from app.config import block_downloads
log = logging.getLogger(__name__)
class InfoView:
@aiohttp_jinja2.template('info.html')
async def info(self, req):
file_id = int(req.match_info["id"])
alias_id = req.match_info['chat']
chat = [i for i in self.chat_ids if i['alias_id'] == alias_id][0]
chat_id = chat['chat_id']
try:
message = await self.client.get_messages(entity=chat_id, ids=file_id)
except:
log.debug(f"Error in getting message {file_id} in {chat_id}", exc_info=True)
message = None
if not message or not isinstance(message, Message):
log.debug(f"no valid entry for {file_id} in {chat_id}")
return {
'found':False,
'reason' : "Resource you are looking for cannot be retrived!",
}
return_val = {}
reply_btns = []
if message.reply_markup:
if isinstance(message.reply_markup, types.ReplyInlineMarkup):
for button_row in message.reply_markup.rows:
btns = []
for button in button_row.buttons:
if isinstance(button, types.KeyboardButtonUrl):
btns.append({'url': button.url, 'text': button.text})
reply_btns.append(btns)
if message.file and not isinstance(message.media, types.MessageMediaWebPage):
file_name = get_file_name(message)
human_file_size = get_human_size(message.file.size)
media = {
'type':message.file.mime_type
}
if 'video/' in message.file.mime_type:
media['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 = message.raw_text
else:
caption = ''
caption_html = Markup.escape(caption).__str__().replace('\n', '<br>')
return_val = {
'found': True,
'name': file_name,
'file_id': file_id,
'human_size': human_file_size,
'media': media,
'caption_html': caption_html,
'title': f"Download | {file_name} | {human_file_size}",
'reply_btns': reply_btns,
'thumbnail': f"/{alias_id}/{file_id}/thumbnail",
'download_url': '#' if block_downloads else f"/{alias_id}/{file_id}/download",
'page_id': alias_id,
'block_downloads': block_downloads
}
elif message.message:
text = message.raw_text
text_html = Markup.escape(text).__str__().replace('\n', '<br>')
return_val = {
'found': True,
'media': False,
'text_html': text_html,
'reply_btns': reply_btns,
'page_id': alias_id
}
else:
return_val = {
'found':False,
'reason' : "Some kind of resource that I cannot display",
}
log.debug(f"data for {file_id} in {chat_id} returned as {return_val}")
return return_val

63
app/views/logo_view.py Normal file
View File

@@ -0,0 +1,63 @@
import logging
from PIL import Image, ImageDraw
import random
import os
from aiohttp import web
from telethon.tl import types
from app.config import logo_folder
log = logging.getLogger(__name__)
class LogoView:
async def logo(self, req):
alias_id = req.match_info['chat']
chat = [i for i in self.chat_ids if i['alias_id'] == alias_id][0]
chat_id = chat['chat_id']
chat_name = "Image not available"
logo_path = logo_folder.joinpath(f"{alias_id}.jpg")
if not logo_path.exists():
try:
photo = await self.client.get_profile_photos(chat_id)
except:
log.debug(f"Error in getting profile picture in {chat_id}", exc_info=True)
photo = None
if not photo:
W, H = (160, 160)
color = tuple([random.randint(0, 255) for i in range(3)])
im = Image.new("RGB", (W,H), color)
draw = ImageDraw.Draw(im)
w, h = draw.textsize(chat_name)
draw.text(((W-w)/2,(H-h)/2), chat_name, fill="white")
im.save(logo_path)
else:
photo = photo[0]
pos = -1 if req.query.get('big', None) else int(len(photo.sizes)/2)
size = self.client._get_thumb(photo.sizes, pos)
if isinstance(size, (types.PhotoCachedSize, types.PhotoStrippedSize)):
await self.client._download_cached_photo_size(size, logo_path)
else:
media = types.InputPhotoFileLocation(
id=photo.id,
access_hash=photo.access_hash,
file_reference=photo.file_reference,
thumb_size=size.type
)
await self.client.download_file(media, logo_path)
with open(logo_path, 'rb') as fp:
body = fp.read()
return web.Response(
status=200,
body=body,
headers={
"Content-Type": "image/jpeg",
"Content-Disposition": 'inline; filename="logo.jpg"'
}
)

View File

@@ -0,0 +1,71 @@
import logging
from PIL import Image
import random
import io
from aiohttp import web
from telethon.tl import types
log = logging.getLogger(__name__)
class ThumbnailView:
async def thumbnail_get(self, req):
file_id = int(req.match_info["id"])
alias_id = req.match_info['chat']
chat = [i for i in self.chat_ids if i['alias_id'] == alias_id][0]
chat_id = chat['chat_id']
try:
message = await self.client.get_messages(entity=chat_id, ids=file_id)
except:
log.debug(f"Error in getting message {file_id} in {chat_id}", exc_info=True)
message = None
if not message or not message.file:
log.debug(f"no result for {file_id} in {chat_id}")
return web.Response(status=410, text="410: Gone. Access to the target resource is no longer available!")
if message.document:
media = message.document
thumbnails = media.thumbs
location = types.InputDocumentFileLocation
else:
media = message.photo
thumbnails = media.sizes
location = types.InputPhotoFileLocation
if not thumbnails:
color = tuple([random.randint(0, 255) for i in range(3)])
im = Image.new("RGB", (100, 100), color)
temp = io.BytesIO()
im.save(temp, "PNG")
body = temp.getvalue()
else:
thumb_pos = int(len(thumbnails)/2)
thumbnail = self.client._get_thumb(thumbnails, thumb_pos)
if not thumbnail or isinstance(thumbnail, types.PhotoSizeEmpty):
return web.Response(status=410, text="410: Gone. Access to the target resource is no longer available!")
if isinstance(thumbnail, (types.PhotoCachedSize, types.PhotoStrippedSize)):
body = self.client._download_cached_photo_size(thumbnail, bytes)
else:
actual_file = location(
id=media.id,
access_hash=media.access_hash,
file_reference=media.file_reference,
thumb_size=thumbnail.type
)
body = self.client.iter_download(actual_file)
r = web.Response(
status=200,
body=body,
headers={
"Content-Type": "image/jpeg",
"Content-Disposition": 'inline; filename="thumbnail.jpg"'
}
)
return r

View File

@@ -0,0 +1,7 @@
from aiohttp import web
class WildcardView:
async def wildcard(self, req):
raise web.HTTPFound('/')