Skip to content

Commit

Permalink
feat: telegram mini-app (#282)
Browse files Browse the repository at this point in the history
* feat: added a telegram mini-app

* running web server in main

* fix: applied suggestions and pylint complaints
  • Loading branch information
TaToTanWeb authored Sep 3, 2024
1 parent 2ec5b9e commit 4105e39
Show file tree
Hide file tree
Showing 27 changed files with 5,518 additions and 71 deletions.
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -130,3 +130,7 @@ pydrive/*
.idea
data/json/subjs.json
**/.DS_Store

# Webapp
node_modules/
.parcel-cache/
4 changes: 3 additions & 1 deletion main.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@
from module.shared import config_map
from module.utils.multi_lang_utils import load_translations, get_regex_multi_lang
from module.debug import error_handler, log_message

from webapp.app import app
import uvicorn

def add_commands(up: Updater) -> None:
"""Adds the list of commands with their description to the bot
Expand Down Expand Up @@ -195,6 +196,7 @@ def main() -> None:
add_jobs(updater.dispatcher)

updater.start_polling()
uvicorn.run(app, host="0.0.0.0", port=8000)
updater.idle()


Expand Down
88 changes: 22 additions & 66 deletions module/commands/gdrive.py
Original file line number Diff line number Diff line change
@@ -1,33 +1,12 @@
"""/drive command"""
import os

import yaml
from pydrive2.auth import AuthError, GoogleAuth
from pydrive2.drive import GoogleDrive
from pydrive2.files import GoogleDriveFile
from telegram import InlineKeyboardButton, InlineKeyboardMarkup, Update, Bot
from telegram.ext import CallbackContext
from module.shared import check_log
from module.debug import log_error
from module.data.vars import TEXT_IDS, PLACE_HOLDER
from module.utils.multi_lang_utils import get_locale

gdrive_interface = None

with open('config/settings.yaml', 'r', encoding='utf-8') as yaml_config:
config_map = yaml.load(yaml_config, Loader=yaml.SafeLoader)


def get_gdrive_interface() -> GoogleDrive:
global gdrive_interface

if gdrive_interface is None:
# gauth uses all the client_config of settings.yaml
gauth = GoogleAuth(settings_file="config/settings.yaml")
gauth.CommandLineAuth()
gdrive_interface = GoogleDrive(gauth)

return gdrive_interface
from module.utils.drive_utils import drive_utils


def drive(update: Update, context: CallbackContext) -> None:
Expand All @@ -39,7 +18,6 @@ def drive(update: Update, context: CallbackContext) -> None:
context: context passed by the handler
"""
check_log(update, "drive")
gdrive: GoogleDrive = get_gdrive_interface()
chat_id: int = update.message.chat_id
locale: str = update.message.from_user.language_code
if chat_id < 0:
Expand All @@ -48,24 +26,20 @@ def drive(update: Update, context: CallbackContext) -> None:
)
return

try:
file_list = gdrive.ListFile(
{
'q': f"'{config_map['drive_folder_id']}' in parents and trashed=false",
'orderBy': 'folder,title',
}
).GetList()

except AuthError as err:
log_error(header="drive", error=err)

# keyboard that allows the user to navigate the folder
keyboard = get_files_keyboard(file_list, row_len=3)
context.bot.sendMessage(
chat_id=chat_id,
text=get_locale(locale, TEXT_IDS.DRIVE_HEADER_TEXT_ID),
reply_markup=InlineKeyboardMarkup(keyboard),
)
file_list = drive_utils.list_files()
if file_list:
# keyboard that allows the user to navigate the folder
keyboard = get_files_keyboard(file_list, row_len=3)
context.bot.sendMessage(
chat_id=chat_id,
text=get_locale(locale, TEXT_IDS.DRIVE_HEADER_TEXT_ID),
reply_markup=InlineKeyboardMarkup(keyboard),
)
else:
context.bot.sendMessage(
chat_id=chat_id,
text=get_locale(locale, TEXT_IDS.DRIVE_ERROR_DEVS_TEXT_ID),
)


def drive_handler(update: Update, context: CallbackContext) -> None:
Expand All @@ -78,28 +52,17 @@ def drive_handler(update: Update, context: CallbackContext) -> None:
"""
bot: Bot = context.bot

gdrive: GoogleDrive = get_gdrive_interface()

query_data: str = update.callback_query.data.replace("drive_file_", "")
chat_id: int = update.callback_query.from_user.id
message_id: int = update.callback_query.message.message_id
locale: str = update.callback_query.from_user.language_code
fetched_file: GoogleDriveFile = gdrive.CreateFile({'id': query_data})
fetched_file: GoogleDriveFile = drive_utils.get_file(query_data)

# the user clicked on a folder
if fetched_file['mimeType'] == "application/vnd.google-apps.folder":
try:
istance_file = gdrive.ListFile(
{
'q': f"'{fetched_file['id']}' in parents and trashed=false",
'orderBy': 'folder,title',
}
)
file_list = istance_file.GetList()
file_list = drive_utils.list_files(fetched_file['id'])

# pylint: disable=broad-except
except Exception as e:
log_error(header="drive_handler", error=e)
if file_list is None:
bot.editMessageText(
chat_id=chat_id,
message_id=message_id,
Expand Down Expand Up @@ -139,19 +102,12 @@ def drive_handler(update: Update, context: CallbackContext) -> None:

else: # the user clicked on a file
try:
file_d = gdrive.CreateFile({'id': fetched_file['id']})

file_d = drive_utils.get_file(fetched_file['id'])
if int(file_d['fileSize']) < 5e7:

file_path = f"file/{fetched_file['title']}"
file_d.GetContentFile(file_path)

f = file_d.GetContentIOBuffer()
f.name = fetched_file['title']
bot.sendChatAction(chat_id=chat_id, action="UPLOAD_DOCUMENT")

with open(file_path, 'rb') as f:
bot.sendDocument(chat_id=chat_id, document=f)

os.remove(file_path)
bot.sendDocument(chat_id=chat_id, document=f)

else:
bot.sendMessage(
Expand Down
4 changes: 2 additions & 2 deletions module/commands/lezioni.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,8 @@ def get_url(courses: str) -> str:
endl_index = courses.find("\n", course_index)

if token_pos != -1:
main_link = courses[token_pos + 1:endl_index]
return main_link
return courses[token_pos + 1:endl_index]
return ''


def get_orario_file() -> Optional[bytes]:
Expand Down
2 changes: 1 addition & 1 deletion module/data/lesson.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ def scrape(cls, year_exams: str, delete: bool = False):

if soup.find('b', id='attivo').text[0] == 'S':
semestre = 2
elif soup.find('b', id='attivo').text[0] == 'P':
else:
semestre = 1

table = soup.find('table', id='tbl_small_font')
Expand Down
43 changes: 43 additions & 0 deletions module/utils/drive_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import yaml
from typing import Optional
from pydrive2.auth import GoogleAuth
from pydrive2.drive import GoogleDrive
from pydrive2.files import GoogleDriveFileList, GoogleDriveFile
from module.debug import log_error


class DriveUtils:
def __init__(self):
self._gdrive = None
with open('config/settings.yaml', 'r', encoding='utf-8') as yaml_config:
self.config_map = yaml.load(yaml_config, Loader=yaml.SafeLoader)

@property
def gdrive(self) -> GoogleDrive:
'Returns the active drive.GoogleDrive instance.'
if self._gdrive is None:
# gauth uses all the client_config of settings.yaml
gauth = GoogleAuth(settings_file="./config/settings.yaml")
gauth.CommandLineAuth()
self._gdrive = GoogleDrive(gauth)
return self._gdrive

def list_files(self, folder_id: Optional[str] = None) -> Optional[GoogleDriveFileList]:
'Returns a list of files or folders in the given directory'
folder_id = folder_id or self.config_map['drive_folder_id']
try:
return self.gdrive.ListFile({
'q': f"'{folder_id}' in parents and trashed=false",
'orderBy': 'folder,title',
}).GetList()
# pylint: disable=broad-except
except Exception as e:
log_error(header="drive_handler", error=e)
return None

def get_file(self, file_id: str) -> GoogleDriveFile:
'Shorthand for self.gdrive.CreateFile'
return self.gdrive.CreateFile({'id': file_id})


drive_utils = DriveUtils()
4 changes: 3 additions & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
numpy==1.26.4
python-telegram-bot==13.8.1
PyDrive2==1.10.0
requests==2.26.0
Expand All @@ -6,4 +7,5 @@ python-gitlab==2.10.1
matplotlib==3.5.0
pandas==1.3.4
Pillow==8.4.0
lxml==4.6.4
lxml==4.6.4
uvicorn==0.30.6
5 changes: 5 additions & 0 deletions webapp/.postcssrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"plugins": {
"tailwindcss": {}
}
}
50 changes: 50 additions & 0 deletions webapp/app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
from fastapi import FastAPI
from fastapi.staticfiles import StaticFiles
from fastapi.responses import FileResponse, JSONResponse, StreamingResponse
from pydrive2.files import MediaIoReadable
from starlette.responses import ContentStream
from module.utils.drive_utils import drive_utils


app = FastAPI()

@app.get('/favicon.ico')
def favicon():
'Serve the website favicon'
return FileResponse('webapp/static/assets/logo.ico')

@app.get('/drive/folder')
def _(folder_id: str):
'Returns content of a folder in the DMI Drive.'
files = drive_utils.list_files(folder_id) or []
keys = 'id', 'title', 'mimeType'
response = JSONResponse([{key: file[key] for key in keys} for file in files])
response.headers['Access-Control-Allow-Origin'] = '*'
return response

@app.get('/drive/file')
def _(file_id: str):
'Returns content of a file in the DMI Drive.'
file = drive_utils.get_file(file_id)
if not file:
return JSONResponse({'error': 'File not found.'}, status_code = 204)
content = file.GetContentIOBuffer()
return StreamingResponse(
stream(content),
media_type = file['mimeType'],
headers = {
# delivering it as a download
'Content-Disposition': f"attachment; filename=\"{file['title']}\"",
# making it accessible from web browsers
'Access-Control-Allow-Origin': '*'
}
)

def stream(content: MediaIoReadable) -> ContentStream:
chunk = True
while chunk:
chunk = content.read()
if chunk:
yield chunk

app.mount('/', StaticFiles(directory = 'webapp/dist/', html = True), name = 'dist')
48 changes: 48 additions & 0 deletions webapp/drive/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<!DOCTYPE html>
<html>
<head>
<title>DMI Drive</title>
<meta name = "viewport" content = "width=device-width, initial-scale=1" />
<link rel = "stylesheet" href = "static/style.css">
<script src = "https://telegram.org/js/telegram-web-app.js"></script>
<link href='https://unpkg.com/[email protected]/icons/css/folder.css' rel='stylesheet'>
<script type = "module" src = "static/scripts/index.ts"></script>
<script type = "module" src = "static/scripts/back.ts"></script>
</head>
<body>
<header class = "w-full mt-10">
<div class = "w-full flex flex-row justify-center">
<img width = "100" src = "https://upload.wikimedia.org/wikipedia/commons/1/12/Google_Drive_icon_%282020%29.svg">
<!-- <img width = "150" src = "../static/assets/logo.png"> -->
</div>
</header>
<div class = "w-full flex flex-col items-center justify-center">
<folders-div id = "folders-list">
<drive-back-button class = "folder hidden" id = "back-button"></drive-back-button>
<!-- <folder id = "folder" class = "bg-neutral-200 w-full h-10 p-6 flex flex-row items-center shadow-md hover:shadow-lg transition-shadow cursor-pointer rounded-md">
<i class = "mr-5 gg-folder text-neutral-500"></i>
<folder-name class = "font-bold text-neutral-700">Informatica</folder-name>
</folder> -->
</folders-div>
<files-div id = "files-list">
<!-- <file id = "file" class = "bg-neutral-300 flex flex-col p-5 shadow-md hover:shadow-lg transition-shadow cursor-pointer rounded-md">
<img class = "w-32 rounded-md rounded-bl-[28px]" src = "https://telegra.ph/file/cc429f1e5d235f54a4500.png">
<file-name class = "mt-5 font-bold text-neutral-900">Appunti.pdf</file-name>
</file> -->
</files-div>
</div>
</body>
<!-- <pwa>
<meta name = "theme-color" content = "#ffffff">
<link rel = "manifest" href = "static/web/manifest.json">
<link sizes = "500x500" rel = "apple-touch-icon" href = "static/assets/logo.png">
<script type = "module">
document.addEventListener('DOMContentLoaded', () => {
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register(new URL('static/web/sw.js', import.meta.url), {scope: '/'});
}
});
</script>
</pwa> -->
</html>
33 changes: 33 additions & 0 deletions webapp/drive/static/scripts/back.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { Folder, FOLDERMIME } from "./folder";
import { update_content } from "./update";

export class BackButton extends Folder {
foldersHistory = new Array<string>();

connectedCallback(): void {
this.update({
title: '..',
id: 'back',
mimeType: FOLDERMIME
})
}

onFolderChange(folderId: string): void {
this.foldersHistory.push(folderId);
if (folderId != '') {
this.show();
} else {
this.hide();
}
}

override onClick(): void {
const previousFolderId = this.foldersHistory[this.foldersHistory.length - 2];
// Removing the last folder id in the history
this.foldersHistory.splice(this.foldersHistory.length - 2);
// Showing content of the previous folder
update_content(previousFolderId);
}
}

window.customElements.define('drive-back-button', BackButton);
Loading

0 comments on commit 4105e39

Please sign in to comment.