Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .env_example
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
TOKEN="tgtoken"
LOG_DIR="./logs"
POSTGRES_USER=dbuser
POSTGRES_PASSWORD=dbpass
POSTGRES_DB=dbname
Expand Down
2 changes: 2 additions & 0 deletions .flake8
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[flake8]
max-line-length = 85
7 changes: 4 additions & 3 deletions .github/workflows/docker-build-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,17 +19,18 @@ jobs:

- name: Run Docker Container
run: |
docker run -d --name telegram_bot telegram_bot_docker
docker run -d --name telegram_bot -e STATUS_FILE_PATH=/tmp/bot_status.txt telegram_bot_docker

- name: Sleep For a Short Time
run: sleep 30s # Wait for a short time to let the bot start

- name: Check Logs for Bot Start
- name: Check Status File for Bot Start
run: |
if docker logs telegram_bot | grep -q "INFO:aiogram.dispatcher:Start polling"; then
if docker exec telegram_bot [ -f /tmp/bot_status.txt ]; then
echo "Bot started successfully"
docker stop telegram_bot
else
echo "Bot did not start successfully"
docker logs telegram_bot # Print the logs for debugging
exit 1
fi
9 changes: 8 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@
__pycache__/
*.py[cod]

# Ignore all contents of the logs directory
logs/*

# But do not ignore the logs directory itself
!logs/

# C extensions
*.so
Expand All @@ -24,4 +29,6 @@ var/

venv*
dump*
.env
.env

.idea
8 changes: 7 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,16 @@ A Python-based Telegram bot leveraging GCP to organize and share academic resour
1. Create a copy of .env_example and rename it to .env.
2. Fill in all the necessary variables in the .env file.

## Running the Application
## Running the Application without docker compose (no db, no loggin on main machine)
To build the Docker image and start the application, run the following commands:
```py
docker build -t telegram_bot .
docker run -it --name Telegram_bot telegram_bot
```

## Running the Application with docker compose
```py
docker-compose down
docker-compose up --build
```
<p align="right">(<a href="#readme-top">back to top</a>)</p>
14 changes: 11 additions & 3 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -1,17 +1,25 @@
version: 'X'
version: '3.8'

services:
bot:
build: .
container_name: telegram_bot
volumes:
- ${LOG_DIR}:/usr/src/app/logs # Use LOG_DIR environment variable
environment:
- TOKEN=${TOKEN}
- LOG_DIR=/usr/src/app/logs # Set the LOG_DIR environment variable inside the container

postgres:
image: postgres
volumes:
- postgres-data:/var/lib/postgresql/data
- postgres-data:/var/lib/postgresql/data
ports:
- "${POSTGRES_PORTS}"
environment:
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: ${POSTGRES_DB}

volumes:
postgres-data:
postgres-data:
85 changes: 85 additions & 0 deletions src/Logger/FileSystemLoguruLogger.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
from loguru import logger
import os
import sys
from src.helperModules.definitions import MB
from src.Logger.Logger import Logger


class FileSystemLoguruLogger(Logger):
FILE_SYSTEM_LOGGER_FILE_SIZE = 500 * MB

def __init__(self):
log_dir = os.getenv('LOG_DIR', '/usr/src/app/logs')
if not os.path.exists(log_dir):
os.makedirs(log_dir)
self._log_path = os.path.join(log_dir, "application.log")
print(f"Log path: {self._log_path}")

# Remove any default handlers
logger.remove()

# Standard logging configuration
logger.add(
sys.stderr,
level="DEBUG",
format="<green>{time}</green> <level>{level}</level> "
"<cyan>{name}</cyan>:<cyan>{function}</cyan>:"
"<cyan>{line}</cyan> - <level>{message}</level>",
filter=lambda record: not record["extra"].get("metadata_block", False)
)
logger.add(
self._log_path,
format="<green>{time}</green> <level>{level}</level> "
"<cyan>{name}</cyan>:<cyan>{function}</cyan>:"
"<cyan>{line}</cyan> - <level>{message}</level>",
filter=lambda record: not record["extra"].get("metadata_block", False),
rotation=self._rotate_logs,
retention="10 days"
)

# Special handler for metadata block
logger.add(
sys.stderr,
level="DEBUG",
format="{message}",
filter=lambda record: record["extra"].get("metadata_block", False)
)
logger.add(
self._log_path,
format="{message}",
filter=lambda record: record["extra"].get("metadata_block", False),
rotation=self._rotate_logs,
retention="10 days"
)

logger.info(f"Logger initialized. Log path: {self._log_path}")

def log_error(self, error: Exception):
logger.error(error)

def log_warning(self, message):
logger.warning(message)

def log_info(self, message, **kwargs):
if kwargs:
logger.bind(**kwargs).info(message)
else:
logger.bind(metadata_block=False).info(message)

def log_debug(self, message):
logger.debug(message)

def log_critical(self, message):
logger.critical(message)

def _rotate_logs(self, message, file):
file.seek(0, os.SEEK_END)
if file.tell() > self.FILE_SYSTEM_LOGGER_FILE_SIZE:
return True # Rotate the log file
return False # Don't rotate the log file

def context(self, **kwargs):
return logger.contextualize(**kwargs)


MainLogger = FileSystemLoguruLogger()
23 changes: 23 additions & 0 deletions src/Logger/Logger.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
from abc import abstractmethod, ABCMeta


class Logger(metaclass=ABCMeta):
@abstractmethod
def log_error(self, error):
"""Log an error"""

@abstractmethod
def log_warning(self, message):
"""Log a warning"""

@abstractmethod
def log_info(self, message):
"""Log an info"""

@abstractmethod
def log_debug(self, message):
"""Log a debug message"""

@abstractmethod
def log_critical(self, message):
"""Log a critical message"""
Empty file added src/Logger/__init__.py
Empty file.
13 changes: 13 additions & 0 deletions src/Logger/exceptionHandler.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
from functools import wraps
from src.Logger.FileSystemLoguruLogger import MainLogger


def handle_exceptions(func):
@wraps(func)
def wrapper(*args, **kwargs):
try:
return func(*args, **kwargs)
except Exception as e:
MainLogger.log_error(f"Exception in {func.__name__}: {e}")
raise
return wrapper
77 changes: 46 additions & 31 deletions src/bot/file_receiver.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,22 @@
import logging
from aiogram import types
import json

# helper modules
from src.helperModules.definitions import MB

# our modules
from src.helperModules.message_helper import MessageHelper
from src.Logger.FileSystemLoguruLogger import MainLogger


class FileReceiver:
MB = 1024 * 1024 # Bytes in megabyte
DOCUMENT_SIZE_LIMIT = 100 * MB # 100 MB
VIDEO_SIZE_LIMIT = 500 * MB # 500 MB

async def classify_and_handle_message(self, message: types.Message):
try:
message_type = MessageHelper.determine_message_type(message)
MainLogger.log_info(f"Message type determined: {message_type}")

if message_type == "link":
await self.handle_link(message)
Expand All @@ -24,35 +28,43 @@ async def classify_and_handle_message(self, message: types.Message):
await self.handle_unknown_content(message)

except Exception as e:
logging.error(f"Error occurred: {e}")
MainLogger.log_error(f"Error occurred: {e}")
await message.answer("Error processing the message.")

async def handle_link(self, message: types.Message):
# TODO Handle link-specific logic
MainLogger.log_info("Handling link")
metadata = self.create_metadata(message)
self.print_metadata(metadata)
self.log_metadata(metadata)
await message.answer("This is a link")

async def handle_document(self, message: types.Message):
# TODO Handle document-specific logic, including size check
if message.document.file_size > self.DOCUMENT_SIZE_LIMIT:
await message.answer("Document size exceeds 100 MB limit.")
return
metadata = self.create_metadata(message)
self.print_metadata(metadata)
# TODO Further processing (e.g., storage)
MainLogger.log_info("Handling document")
try:
if message.document.file_size > self.DOCUMENT_SIZE_LIMIT:
await message.answer("Document size exceeds 100 MB limit.")
return
metadata = self.create_metadata(message)
self.log_metadata(metadata)
await message.answer("Document received")
except Exception as e:
MainLogger.log_error(f"Error handling document: {e}")
await message.answer("Error processing the document.")

async def handle_video(self, message: types.Message):
# TODO Handle video-specific logic, including size check
if message.video.file_size > self.VIDEO_SIZE_LIMIT:
await message.answer("Video size exceeds 500 MB limit.")
return
metadata = self.create_metadata(message)
self.print_metadata(metadata)
# TODO Further processing (e.g., storage)
MainLogger.log_info("Handling video")
try:
if message.video.file_size > self.VIDEO_SIZE_LIMIT:
await message.answer("Video size exceeds 500 MB limit.")
return
metadata = self.create_metadata(message)
self.log_metadata(metadata)
await message.answer("Video received")
except Exception as e:
MainLogger.log_error(f"Error handling video: {e}")
await message.answer("Error processing the video.")

async def handle_unknown_content(self, message: types.Message):
# TODO Handle unknown content type logic
MainLogger.log_info("Handling unknown content")
await message.answer("Unknown content type")

def create_metadata(self, message: types.Message):
Expand All @@ -64,12 +76,9 @@ def create_metadata(self, message: types.Message):
else:
sender_username = "No username"

if sender:
sender_name = sender.first_name
if sender.last_name:
sender_name += f" {sender.last_name}"
else:
sender_name = "No name"
sender_name = sender.first_name if sender else "No name"
if sender and sender.last_name:
sender_name += f" {sender.last_name}"

message_type = MessageHelper.determine_message_type(message)
iso_formatted_date = message.date.isoformat()
Expand All @@ -86,9 +95,16 @@ def create_metadata(self, message: types.Message):

return metadata

def print_metadata(self, metadata):
for key, value in metadata.items():
print(f"{key}: {value}")
def log_metadata(self, metadata):
with MainLogger.context(metadata_block=True):
metadata_log = {
"event": "metadata_logging",
"details": metadata
}
MainLogger.log_info(
json.dumps(metadata_log, indent=2),
metadata_block=True
)

def get_file_size(self, message: types.Message):
message_type = MessageHelper.determine_message_type(message)
Expand All @@ -98,5 +114,4 @@ def get_file_size(self, message: types.Message):
elif message_type == "video":
return message.video.file_size
else:
# No file size for other types (like link or text)
return 0
return 0 # No file size for other types (like link or text)
Loading