GIGACOMMIT

Changes:
	Frontend:
		- Added and adjusted command handlers for /start, /cat,
	  	/subscribe, /unsubscribe, and /subscription_modify
		- Implemented mass send function to send images for all
	  	subscribed users
		- Configured scheduler to call function above at 12:00 every day
		- Added psycopg2 error exception for each handler

	Backend:
		- Changed PostgreSQL DB Users table structure(id,
		  chat_id, images_amount) and adjusted all functions'
		  arguments and executes to match these changes
		- Added a function to fetch selected images_amount for a
		  set chat_id
		- Slightly optimized GetObjectExtension function
		- Allowed all functions that work with currentDay to
		  accept int values
		- Added a script for batch population of MinIO bucket
		  with images from ./files/<day number>/ directory
	Src:
		- Updated requirements.txt
This commit is contained in:
2025-08-02 18:58:12 +03:00
parent 76bf8f9f26
commit 3692c0233d
6 changed files with 189 additions and 41 deletions

Binary file not shown.

View File

@ -5,10 +5,11 @@ from src import config
def get_last_id(cursor): def get_last_id(cursor):
cursor.execute("SELECT MAX(id) FROM Users") cursor.execute("SELECT MAX(id) FROM Users")
id = cursor.fetchall()[0][0] id = cursor.fetchall()[0][0]
if id == None: if id is None:
return 0 return 0
return id return id
def set_connection(): def set_connection():
connection = psycopg2.connect( connection = psycopg2.connect(
dbname = config.db_name, dbname = config.db_name,
@ -20,24 +21,35 @@ def set_connection():
cursor = connection.cursor() cursor = connection.cursor()
return cursor, connection return cursor, connection
def close_connection(connection, cursor): def close_connection(connection, cursor):
cursor.close() cursor.close()
connection.close() connection.close()
#Functions don't close connection automatically, it has to be closed manually #Functions don't close connection automatically, it has to be closed manually
def add_user(username, chat_id, connection, cursor): def add_user(chat_id, connection, cursor):
cursor.execute("INSERT INTO Users VALUES (%s, %s, %s);", (get_last_id(cursor) + 1, username, chat_id)) cursor.execute("INSERT INTO Users VALUES (%s, %s, %s);", (get_last_id(cursor) + 1, chat_id, 1))
connection.commit() connection.commit()
def delete_user(username, connection, cursor):
cursor.execute("DELETE FROM Users WHERE username = %s;", (username,)) def delete_user(chat_id, connection, cursor):
cursor.execute("DELETE FROM Users WHERE chat_id = %s;", (chat_id,))
connection.commit() connection.commit()
def change_name(old_username, new_username, connection, cursor):
cursor.execute("UPDATE Users SET username = %s WHERE username = %s;", (new_username, old_username)) def change_images_amount(chat_id, amount, connection, cursor):
cursor.execute('UPDATE Users SET images_amount = %s WHERE chat_id = %s;', (amount, chat_id))
connection.commit() connection.commit()
def get_images_amount(chat_id, connection, cursor):
cursor.execute('SELECT images_amount FROM Users WHERE chat_id = %s;', (chat_id,))
images_amount = cursor.fetchall()[0][0]
return images_amount
def get_chat_id(id, cursor): def get_chat_id(id, cursor):
cursor.execute("SELECT chatid FROM Users WHERE id = %s", (id,)) cursor.execute("SELECT chat_id FROM Users WHERE id = %s", (id,))
chat_id = cursor.fetchall()[0][0] chat_id = cursor.fetchall()[0][0]
return chat_id return chat_id

View File

@ -1,7 +1,6 @@
from minio import Minio from minio import Minio
from random import randint from random import randint
from datetime import timedelta from datetime import timedelta
from src.Backend import DBwork
from src import config from src import config
@ -14,38 +13,36 @@ def _setClient():
) )
return minio_client return minio_client
def getNumberofObjects(client, currentDay): def getNumberofObjects(client, currentDay):
objects = client.list_objects(config.bucket_name, prefix=currentDay+'/') objects = client.list_objects(config.bucket_name, prefix=str(currentDay) + '/')
return sum(1 for _ in objects) return sum(1 for _ in objects)
def getObjectExtension(client, currentDay, fileNumber): def getObjectExtension(client, currentDay, fileNumber):
objects = client.list_objects(config.bucket_name, prefix=currentDay+'/') objects = client.list_objects(config.bucket_name, prefix=str(currentDay) + '/')
for counter, obj in enumerate(objects, start=1): counter = 0
object_extension = None
for obj in objects:
counter += 1
if counter == fileNumber: if counter == fileNumber:
return obj.object_name.split('.')[-1] object_extension = obj.object_name.split('.')[-1]
return object_extension
def getImageName(currentDay, client): def getImageName(currentDay, client):
maxFiles = getNumberofObjects(client, currentDay) maxFiles = getNumberofObjects(client, currentDay)
fileNumber = randint(1, maxFiles) fileNumber = randint(1, maxFiles)
fileExtension = '.' + getObjectExtension(client, currentDay, fileNumber) fileExtension = '.' + getObjectExtension(client, currentDay, fileNumber)
desiredFile = currentDay + '/' + str(fileNumber) + fileExtension desiredFile = str(currentDay) + '/' + str(fileNumber) + fileExtension
return desiredFile return desiredFile
def getDownloadURL(currentDay): def getDownloadURL(currentDay):
client = _setClient() client = _setClient()
object_name = getImageName(currentDay, client) object_name = getImageName(currentDay, client)
url = client.presigned_get_object( url = client.presigned_get_object(
config.bucket_name, config.bucket_name,
object_name, object_name,
expires=timedelta(days=1)) expires=timedelta(days=1)
)
return url return url
def downloadForAll(currentDay):
cur, conn = DBwork.set_connection()
max_id = DBwork.get_last_id(cur)
for id in range(1, max_id + 1):
chat_id = DBwork.get_chat_id(id, cur)
image_URL = getDownloadURL(currentDay)
# await bot.send_photo(chat_id = chat_id, photo = image_URL
DBwork.close_connection(conn, cur)

View File

@ -0,0 +1,58 @@
from minio import Minio
from minio.error import S3Error
from src import config
import glob
import os
access_key = config.acc_key
secret_key = config.sec_key
bucket = config.bucket_name
def upload_file(access_key, secret_key, bucket_name, object_name, file_path):
try:
client = Minio(
config.IS_address,
access_key = access_key,
secret_key = secret_key,
secure = False
)
if file_path.split('.')[-1] == 'jpeg':
client.fput_object(bucket_name, object_name, file_path, 'image/jpeg')
if file_path.split('.')[-1] == 'jpg':
client.fput_object(bucket_name, object_name, file_path, 'image/jpg')
elif file_path.split('.')[-1] == 'png':
client.fput_object(bucket_name, object_name, file_path, 'image/png')
elif file_path.split('.')[-1] == 'pjpeg':
client.fput_object(bucket_name, object_name, file_path, 'image/pjpeg')
print(f'image {object_name} with local path \'{file_path}\' was successfully uploaded')
except S3Error:
print(f'Error during MinIO operation: {S3Error}')
paths = glob.glob('../../files/*/*')
shortened_paths = []
for i in range(len(paths)):
paths[i] = os.path.abspath(paths[i]).replace('\\', '/')
for i in range(len(paths)):
shortened_paths.append(paths[i][paths[i].find('files/') + 6:])
# for i in range(0, len(paths)):
# upload_file(access_key, secret_key, bucket, f'{shortened_paths[i]}', f'{paths[i]}')
# print(paths[i])
client = Minio(
config.IS_address,
access_key = access_key,
secret_key = secret_key,
secure = False
)
objects = client.list_objects(config.bucket_name, prefix='1' + '/')
for obj in objects:
print(obj.object_name)

View File

@ -1,15 +1,21 @@
import psycopg2
import Backend.ISwork, Backend.DBwork
from src import config from src import config
import asyncio from src.Backend import DBwork
from src.Backend import ISwork
import logging import logging
from aiogram import Bot, Dispatcher, F, Router from aiogram import Bot, Dispatcher, Router
from aiogram.filters import Command from aiogram.filters import Command, CommandObject
from aiogram.types import Message from aiogram.types import Message, URLInputFile
from aiogram.client.default import DefaultBotProperties from aiogram.client.default import DefaultBotProperties
from aiogram.enums import ParseMode from aiogram.enums import ParseMode
from aiogram.fsm.storage.memory import MemoryStorage from aiogram.fsm.storage.memory import MemoryStorage
from apscheduler.schedulers.asyncio import AsyncIOScheduler from apscheduler.schedulers.asyncio import AsyncIOScheduler
from datetime import datetime
current_day = datetime.now().weekday()
start_router = Router() start_router = Router()
scheduler = AsyncIOScheduler(timezone = 'Europe/Moscow') scheduler = AsyncIOScheduler(timezone = 'Europe/Moscow')
@ -17,21 +23,95 @@ scheduler = AsyncIOScheduler(timezone = 'Europe/Moscow')
logging.basicConfig(level = logging.INFO, format = '%(asctime)s - %(name)s - %(levelname)s - %(message)s') logging.basicConfig(level = logging.INFO, format = '%(asctime)s - %(name)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
bot = Bot(token = config.TG_TOKEN , default = DefaultBotProperties(parse_mode = ParseMode.HTML)) bot = Bot(token = config.TG_token , default = DefaultBotProperties(parse_mode = ParseMode.HTML))
dp = Dispatcher(storage = MemoryStorage()) dp = Dispatcher(storage = MemoryStorage())
@dp.message(Command('start')) @dp.message(Command('start'))
async def cmd_start(message: Message): async def cmd_start(message: Message):
await message.answer('Запуск сообщения по команде /start используя фильтр CommandStart()') await message.answer('''
This is a bot that sends images of cats.
List of available commands:
/cat - request an image of a cat from today's pool of images
/subscribe - subscribe to daily cat images sent at 12:00 UTC+3
/subscription_modify <number> - change the amount of images sent daily
/unsubscribe - cancel your subscription(but why would you want to? :3)
''')
@dp.message(F.text) @dp.message(Command('cat'))
async def basic_reaction(message: Message): async def cmd_cat(message: Message):
if message.text != '': image_link = URLInputFile(ISwork.getDownloadURL(current_day), filename=datetime.now().strftime('%Y_%m_%d_%H_%M_%S'))
await message.answer(message.text) await message.answer_photo(image_link, caption='Look, a cat :3')
@dp.message(Command('subscription_modify'))
async def subscription_modify(message: Message, command: CommandObject):
if command.args is None or command.args.isdigit() == False:
await message.answer('Please write the number of images you would like\n'
'to receive in the same message as a command.\n'
'Example:'
'/subscription_modify <number of daily images>')
return
try:
cursor, connection = DBwork.set_connection()
chat_id = message.chat.id
amount = command.args
DBwork.change_images_amount(chat_id, amount, connection, cursor)
DBwork.close_connection(connection, cursor)
except psycopg2.Error:
await message.answer('You are not yet subscribed.')
return
await message.answer('Amount of daily images was changed successfully!')
@dp.message(Command('subscribe'))
async def cmd_subscribe(message: Message):
try:
cursor, connection = DBwork.set_connection()
chat_id = message.chat.id
DBwork.add_user(chat_id, connection, cursor)
DBwork.close_connection(connection, cursor)
except psycopg2.Error as e:
await message.answer('You are already subscribed.')
print(e.pgerror)
return
await message.answer('''
You have successfully subscribed to daily cat photos!
You will get 1 photo a day by default,
use /subscription_modify to change that amount.
''')
@dp.message(Command('unsubscribe'))
async def cmd_unsubscribe(message: Message):
try:
cursor, connection = DBwork.set_connection()
chat_id = message.chat.id
DBwork.delete_user(chat_id, connection, cursor)
DBwork.close_connection(connection, cursor)
except psycopg2.Error:
await message.answer('You are not yet subscribed.')
return
await message.answer('You have successfully unsubscribed.')
async def send_daily_images():
cursor, connection = DBwork.set_connection()
max_id = DBwork.get_last_id(cursor)
for id in range(1, max_id + 1):
chat_id = DBwork.get_chat_id(id, cursor)
images_amount = DBwork.get_images_amount(chat_id, connection, cursor)
for _ in range(images_amount):
image_link = URLInputFile(ISwork.getDownloadURL(current_day), filename=datetime.now().strftime('%Y_%m_%d_%H_%M_%S'))
await bot.send_photo(chat_id = chat_id, photo = image_link)
DBwork.close_connection(connection, cursor)
scheduler.add_job(send_daily_images, 'cron', hour = 12, minute = 0)
async def main(): async def main():
scheduler.start()
await dp.start_polling(bot) await dp.start_polling(bot)

View File

@ -2,8 +2,9 @@ from decouple import config
IS_address = config('IS_ADDRESS') IS_address = config('IS_ADDRESS')
acc_key = config('ACC_KEY') acc_key = config('MINIO_ACCESS_KEY')
sec_key = config('SEC_KEY') sec_key = config('MINIO_SECRET_KEY')
root_user = config('MINIO_ROOT_USER')
db_name = config('DB_NAME') db_name = config('DB_NAME')
postgres_user = config('POSTGRES_USER') postgres_user = config('POSTGRES_USER')
postgres_password = config('POSTGRES_PASSWORD') postgres_password = config('POSTGRES_PASSWORD')
@ -12,5 +13,5 @@ port = config('PORT')
bucket_name = config('BUCKET_NAME') bucket_name = config('BUCKET_NAME')
TG_TOKEN = config('TG_TOKEN') TG_token = config('TG_TOKEN')
# ADMINS = [int(admin_id) for admin_id in config('ADMINS').split(',')] # ADMINS = [int(admin_id) for admin_id in config('ADMINS').split(',')]