add authentication

This commit is contained in:
odysseusmax 2021-05-22 19:30:08 +05:30
parent 85032a0c50
commit 6bbca80da9
19 changed files with 461 additions and 217 deletions

View File

@ -48,9 +48,13 @@ pip3 install -U -r requirements.txt
| `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 `true` to set logging level to debug, info by default.
| `BLOCK_DOWNLOADS` (optional) | Enable downloads or not. If provided, downloads will be disabled. | `BLOCK_DOWNLOADS` (optional) | Enable downloads or not. If any value is provided, downloads will be disabled.
| `RESULTS_PER_PAGE` (optional) | Number of results to be returned per page defaults to 20. | `RESULTS_PER_PAGE` (optional) | Number of results to be returned per page defaults to 20.
| `USERNAME` (optional) | Username for authentication, defaults to `''`.
| `PASSWORD` (optional) | Username for authentication, defaults to `''`.
| `SESSION_COOKIE_LIFETIME` (optional) | Number of minutes, for which authenticated session is valid for, after which user has to login again. defaults to 60.
| `SECRET_KEY` (optional) | Long string for signing the session cookies, required if authentication is enabled.
* **Setting value for `INDEX_SETTINGS`** * **Setting value for `INDEX_SETTINGS`**

View File

@ -44,7 +44,18 @@ host = os.environ.get("HOST", "0.0.0.0")
debug = bool(os.environ.get("DEBUG")) debug = bool(os.environ.get("DEBUG"))
block_downloads = bool(os.environ.get("BLOCK_DOWNLOADS")) block_downloads = bool(os.environ.get("BLOCK_DOWNLOADS"))
results_per_page = int(os.environ.get("RESULTS_PER_PAGE", "20")) results_per_page = int(os.environ.get("RESULTS_PER_PAGE", "20"))
logo_folder = Path( logo_folder = Path("/Temp/logo/" if platform.system() == "Windows" else "/tmp/logo")
"/Temp/logo/" if platform.system() == 'Windows' else '/tmp/logo'
)
logo_folder.mkdir(exist_ok=True) logo_folder.mkdir(exist_ok=True)
username = os.environ.get("USERNAME", "")
password = os.environ.get("PASSWORD", "")
authenticated = username and password
SESSION_COOKIE_LIFETIME = int(os.environ.get("SESSION_COOKIE_LIFETIME") or "60")
try:
SECRET_KEY = os.environ["SECRET_KEY"]
except (KeyError, ValueError):
if authenticated:
traceback.print_exc()
print("\n\nPlease set the SECRET_KEY environment variable correctly")
sys.exit(1)
else:
SECRET_KEY = ""

View File

@ -8,22 +8,54 @@ from aiohttp import web
from .telegram import Client from .telegram import Client
from .routes import setup_routes from .routes import setup_routes
from .views import Views from .views import Views, middleware_factory
from .config import host, port, session_string, api_id, api_hash, debug from .config import (
host,
port,
session_string,
api_id,
api_hash,
authenticated,
username,
password,
SESSION_COOKIE_LIFETIME,
SECRET_KEY,
)
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
def inspect_rep():
@web.middleware
async def factory(request, handler):
print("Incoming Cookies", request.cookies)
response = await handler(request)
print("Outgoing Cookies", response.cookies)
return response
return factory
class Indexer: class Indexer:
TEMPLATES_ROOT = pathlib.Path(__file__).parent / 'templates' TEMPLATES_ROOT = pathlib.Path(__file__).parent / "templates"
def __init__(self): def __init__(self):
self.server = web.Application() self.server = web.Application(
middlewares=[
inspect_rep(),
middleware_factory(),
]
)
self.loop = asyncio.get_event_loop() self.loop = asyncio.get_event_loop()
self.tg_client = Client(session_string, api_id, api_hash) self.tg_client = Client(session_string, api_id, api_hash)
self.server["is_authenticated"] = authenticated
self.server["username"] = username
self.server["password"] = password
self.server["SESSION_COOKIE_LIFETIME"] = SESSION_COOKIE_LIFETIME
self.server["SECRET_KEY"] = SECRET_KEY
async def startup(self): async def startup(self):
await self.tg_client.start() await self.tg_client.start()
@ -36,12 +68,10 @@ class Indexer:
self.server.on_cleanup.append(self.cleanup) self.server.on_cleanup.append(self.cleanup)
async def cleanup(self, *args): async def cleanup(self, *args):
await self.tg_client.disconnect() await self.tg_client.disconnect()
log.debug("telegram client disconnected!") log.debug("telegram client disconnected!")
def run(self): def run(self):
self.loop.run_until_complete(self.startup()) self.loop.run_until_complete(self.startup())
web.run_app(self.server, host=host, port=port) web.run_app(self.server, host=host, port=port)

View File

@ -18,7 +18,12 @@ async def setup_routes(app, handler):
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)] routes = [
web.get("/", h.home, name="home"),
web.get("/login", h.login_get, name="login_page"),
web.post("/login", h.login_post, name="login_handle"),
web.get("/logout", h.logout_get, name="logout"),
]
if index_all: if index_all:
# print(await client.get_dialogs()) # print(await client.get_dialogs())
# dialogs = await client.get_dialogs() # dialogs = await client.get_dialogs()

View File

@ -1,8 +1,10 @@
<footer class="text-center text-black w-full my-4 py-4 bg-gray-300 xl:text-xl"> </div>
<a href="https://github.com/odysseusmax/tg-index" target="_blank"> @odysseusmax </a> <footer class="w-full my-4 py-4 bg-gray-300 xl:text-xl">
</footer> <div class="mx-auto text-center text-black max-w-screen-xl">
<a href="https://github.com/odysseusmax/tg-index" target="_blank"> @odysseusmax </a>
</div>
</footer>
</div> </body>
</body>
</html> </html>

View File

@ -1,20 +1,26 @@
<!doctype html> <!doctype html>
<html lang="en"> <html lang="en">
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link href="https://unpkg.com/tailwindcss@^1.0/dist/tailwind.min.css" rel="stylesheet"> <head>
<script src="https://cdn.fluidplayer.com/v3/current/fluidplayer.min.js"></script> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title> <link href="https://unpkg.com/tailwindcss@^1.0/dist/tailwind.min.css" rel="stylesheet">
{% if title %} {{title}} {% else %} Telegram Index {% endif %} <script src="https://cdn.fluidplayer.com/v3/current/fluidplayer.min.js"></script>
</title>
</head> <title>
<body> {% if title %} {{title}} {% else %} Telegram Index {% endif %}
</title>
<div class="m-auto w-full xl:max-w-screen-xl min-h-screen"> </head>
<header class="flex justify-between bg-red-600 text-white mb-2 p-4 w-full sticky top-0 shadow"> <body>
<a href="/" class="text-left text-xl lg:text-2xl xl:text-3xl"> Telegram Index </a>
</header> <header class="bg-red-600 text-white mb-2 p-4 w-full shadow">
<div class="mx-auto flex justify-between content-center max-w-screen-xl">
<a href="/" class="text-left text-xl lg:text-2xl xl:text-3xl"> Telegram Index </a>
{% if authenticated %} <a href="/logout" class="text-right"> Logout </a> {% else %} {%
endif %}
</div>
</header>
<div class="m-auto w-full min-h-screen max-w-screen-xl">

View File

@ -1,20 +1,21 @@
{% include 'header.html' %} {% include 'header.html' %}
<h1 class=" my-2 text-2xl text-center font-bold text-green-400"> <h1 class=" my-2 text-2xl text-center font-bold text-green-400">
Available Sources Available Sources
</h1> </h1>
<div class="mx-auto my-2 p-2 w-full"> <div class="mx-auto my-2 p-2 w-full">
<div class="block p-4 md:flex md:flex-wrap md:justify-center w-full text-center break-words"> <div class="block p-4 md:flex md:flex-wrap md:justify-items-center w-full text-center break-words">
{% for chat in chats %} {% for chat in chats %}
<a title="{{chat.name}}" href="/{{chat.page_id}}" class="flex flex-col justify-around w-full min-h-full md:w-2/5 lg:w-1/5 rounded my-2 md:mx-1 shadow-md hover:shadow-lg hover:border hover:border-blue-300 hover:bg-blue-100"> <a title="{{chat.name}}" href="/{{chat.page_id}}"
class="mx-auto flex flex-col justify-items-center w-2/3 min-h-full md:w-2/5 lg:w-1/4 rounded my-2 p-2 shadow-md hover:shadow-lg hover:border hover:border-blue-300 hover:bg-blue-100">
<img src="/{{chat.page_id}}/logo?big=1" class="w-full rounded-full "> <img src="/{{chat.page_id}}/logo?big=1" class="w-full rounded-full ">
<div class="p-4 rounded">{{chat.name}}</div> <div class="p-4 rounded">{{chat.name}}</div>
</a> </a>
{% endfor %} {% endfor %}
</div>
</div> </div>
</div>
{% include 'footer.html' %} {% include 'footer.html' %}

44
app/templates/login.html Normal file
View File

@ -0,0 +1,44 @@
{% include 'header.html' %}
<div class="min-h-full flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8">
<div class="max-w-md w-full space-y-8">
<div>
<h2 class="mt-6 text-center text-3xl font-extrabold text-gray-900">
Sign in to view the contents
</h2>
</div>
{% if error %}
<div>
<h2 class="p-2 text-center text-xl text-red-500 rounded border border-l-4 border-red-500">
{{ error }}
</h2>
</div>
{% endif %}
<form class="mt-8 space-y-6" action="/login" method="POST">
<input type="hidden" name="remember" value="true">
<div class="rounded-md shadow-sm -space-y-px">
<div>
<label for="email-address" class="sr-only">Username</label>
<input id="email-address" name="username" type="text" required
class="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-900 placeholder-gray-700 text-gray-900 rounded-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm mb-2"
placeholder="Username">
</div>
<div>
<label for="password" class="sr-only">Password</label>
<input id="password" name="password" type="password" autocomplete="current-password" required
class="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-900 placeholder-gray-700 text-gray-900 rounded-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm mt-2"
placeholder="Password">
</div>
</div>
<div>
<button type="submit"
class="group relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
Sign in
</button>
</div>
</form>
</div>
</div>
{% include 'footer.html' %}

View File

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

View File

@ -10,33 +10,35 @@ log = logging.getLogger(__name__)
class Download: class Download:
async def download_get(self, req): async def download_get(self, req):
return await self.handle_request(req) return await self.handle_request(req)
async def download_head(self, req): async def download_head(self, req):
return await self.handle_request(req, head=True) return await self.handle_request(req, head=True)
async def handle_request(self, req, head=False): async def handle_request(self, req, head=False):
if block_downloads: if block_downloads:
return web.Response(status=403, text="403: Forbiden" if not head else None) return web.Response(status=403, text="403: Forbiden" if not head else None)
file_id = int(req.match_info["id"]) file_id = int(req.match_info["id"])
alias_id = req.match_info['chat'] alias_id = req.match_info["chat"]
chat = [i for i in self.chat_ids if i['alias_id'] == alias_id][0] chat = self.chat_ids[alias_id]
chat_id = chat['chat_id'] chat_id = chat["chat_id"]
try: try:
message = await self.client.get_messages(entity=chat_id, ids=file_id) message = await self.client.get_messages(entity=chat_id, ids=file_id)
except: except Exception:
log.debug(f"Error in getting message {file_id} in {chat_id}", exc_info=True) log.debug(f"Error in getting message {file_id} in {chat_id}", exc_info=True)
message = None message = None
if not message or not message.file: if not message or not message.file:
log.debug(f"no result for {file_id} in {chat_id}") 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) 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 media = message.media
size = message.file.size size = message.file.size
@ -52,14 +54,14 @@ class Download:
return web.Response( return web.Response(
status=416, status=416,
text="416: Range Not Satisfiable" if not head else None, text="416: Range Not Satisfiable" if not head else None,
headers = { headers={"Content-Range": f"bytes */{size}"},
"Content-Range": f"bytes */{size}"
}
) )
if not head: if not head:
body = self.client.download(media, size, offset, limit) body = self.client.download(media, size, offset, limit)
log.info(f"Serving file in {message.id} (chat {chat_id}) ; Range: {offset} - {limit}") log.info(
f"Serving file in {message.id} (chat {chat_id}) ; Range: {offset} - {limit}"
)
else: else:
body = None body = None
@ -68,11 +70,7 @@ class Download:
"Content-Range": f"bytes {offset}-{limit}/{size}", "Content-Range": f"bytes {offset}-{limit}/{size}",
"Content-Length": str(limit - offset), "Content-Length": str(limit - offset),
"Accept-Ranges": "bytes", "Accept-Ranges": "bytes",
"Content-Disposition": f'attachment; filename="{file_name}"' "Content-Disposition": f'attachment; filename="{file_name}"',
} }
return web.Response( return web.Response(status=206 if offset else 200, body=body, headers=headers)
status=206 if offset else 200,
body=body,
headers=headers
)

View File

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

View File

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

View File

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

49
app/views/login_view.py Normal file
View File

@ -0,0 +1,49 @@
import time
import hmac
import hashlib
from aiohttp import web
import aiohttp_jinja2
class LoginView:
@aiohttp_jinja2.template("login.html")
async def login_get(self, req):
return dict(authenticated=False, **req.query)
async def login_post(self, req):
post_data = await req.post()
location = req.app.router["login_page"].url_for()
if "username" not in post_data:
loc = location.with_query({"error": "Username missing"})
raise web.HTTPFound(location=loc)
if "password" not in post_data:
loc = location.with_query({"error": "Password missing"})
raise web.HTTPFound(location=loc)
authenticated = (post_data["username"] == req.app["username"]) and (
post_data["password"] == req.app["password"]
)
if not authenticated:
loc = location.with_query({"error": "Wrong Username or Passowrd"})
raise web.HTTPFound(location=loc)
resp = web.Response(
status=302, headers={"Location": str(req.app.router["home"].url_for())}
)
now = time.time()
resp.set_cookie(
name="_tgindex_session",
value=str(now),
max_age=60 * req.app["SESSION_COOKIE_LIFETIME"],
)
digest = hmac.new(
req.app["SECRET_KEY"].encode(), str(now).encode(), hashlib.sha256
).hexdigest()
resp.set_cookie(
name="_tgindex_secret",
value=digest,
max_age=60 * req.app["SESSION_COOKIE_LIFETIME"],
)
return resp

View File

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

11
app/views/logout_view.py Normal file
View File

@ -0,0 +1,11 @@
from aiohttp import web
class LogoutView:
async def logout_get(self, req):
resp = web.Response(
status=302, headers={"Location": str(req.app.router["home"].url_for())}
)
resp.del_cookie(name="_tgindex_session")
resp.del_cookie(name="_tgindex_secret")
return resp

49
app/views/middlewhere.py Normal file
View File

@ -0,0 +1,49 @@
import time
import hmac
import hashlib
import logging
from aiohttp.web import middleware, HTTPFound
log = logging.getLogger(__name__)
def middleware_factory():
@middleware
async def factory(request, handler):
if request.app["is_authenticated"] and str(request.rel_url.path) not in [
"/login",
"/logout",
]:
cookies = request.cookies
url = request.app.router["login_page"].url_for()
if any(x not in cookies for x in ("_tgindex_session", "_tgindex_secret")):
raise HTTPFound(url)
tgindex_session = cookies["_tgindex_session"]
tgindex_secret = cookies["_tgindex_secret"]
calculated_digest = hmac.new(
request.app["SECRET_KEY"].encode(),
str(tgindex_session).encode(),
hashlib.sha256,
).hexdigest()
if tgindex_secret != calculated_digest:
raise HTTPFound(url)
try:
created_at = (
float(tgindex_session) + request.app["SESSION_COOKIE_LIFETIME"]
)
if (
time.time()
> created_at + 60 * request.app["SESSION_COOKIE_LIFETIME"]
):
raise HTTPFound(url)
except Exception as e:
log.error(e, exc_info=True)
raise HTTPFound(url)
return await handler(request)
return factory

View File

@ -11,21 +11,23 @@ log = logging.getLogger(__name__)
class ThumbnailView: class ThumbnailView:
async def thumbnail_get(self, req): async def thumbnail_get(self, req):
file_id = int(req.match_info["id"]) file_id = int(req.match_info["id"])
alias_id = req.match_info['chat'] alias_id = req.match_info["chat"]
chat = [i for i in self.chat_ids if i['alias_id'] == alias_id][0] chat = self.chat_ids[alias_id]
chat_id = chat['chat_id'] chat_id = chat["chat_id"]
try: try:
message = await self.client.get_messages(entity=chat_id, ids=file_id) message = await self.client.get_messages(entity=chat_id, ids=file_id)
except: except Exception:
log.debug(f"Error in getting message {file_id} in {chat_id}", exc_info=True) log.debug(f"Error in getting message {file_id} in {chat_id}", exc_info=True)
message = None message = None
if not message or not message.file: if not message or not message.file:
log.debug(f"no result for {file_id} in {chat_id}") 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!") return web.Response(
status=410,
text="410: Gone. Access to the target resource is no longer available!",
)
if message.document: if message.document:
media = message.document media = message.document
@ -43,10 +45,13 @@ class ThumbnailView:
im.save(temp, "PNG") im.save(temp, "PNG")
body = temp.getvalue() body = temp.getvalue()
else: else:
thumb_pos = int(len(thumbnails)/2) thumb_pos = int(len(thumbnails) / 2)
thumbnail = self.client._get_thumb(thumbnails, thumb_pos) thumbnail = self.client._get_thumb(thumbnails, thumb_pos)
if not thumbnail or isinstance(thumbnail, types.PhotoSizeEmpty): 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!") return web.Response(
status=410,
text="410: Gone. Access to the target resource is no longer available!",
)
if isinstance(thumbnail, (types.PhotoCachedSize, types.PhotoStrippedSize)): if isinstance(thumbnail, (types.PhotoCachedSize, types.PhotoStrippedSize)):
body = self.client._download_cached_photo_size(thumbnail, bytes) body = self.client._download_cached_photo_size(thumbnail, bytes)
@ -55,17 +60,16 @@ class ThumbnailView:
id=media.id, id=media.id,
access_hash=media.access_hash, access_hash=media.access_hash,
file_reference=media.file_reference, file_reference=media.file_reference,
thumb_size=thumbnail.type thumb_size=thumbnail.type,
) )
body = self.client.iter_download(actual_file) body = self.client.iter_download(actual_file)
r = web.Response( return web.Response(
status=200, status=200,
body=body, body=body,
headers={ headers={
"Content-Type": "image/jpeg", "Content-Type": "image/jpeg",
"Content-Disposition": 'inline; filename="thumbnail.jpg"' "Content-Disposition": 'inline; filename="thumbnail.jpg"',
} },
) )
return r

View File

@ -2,6 +2,5 @@ from aiohttp import web
class WildcardView: class WildcardView:
async def wildcard(self, req): async def wildcard(self, req):
raise web.HTTPFound('/') raise web.HTTPFound("/")