Compare commits

...

21 Commits

Author SHA1 Message Date
cb0731feba Docker build push
- Pushed a docker image
- Changed some .env variables' names(check config.py)
- Fixed minioPopulator.py
- Added more NoneType checks for bot handlers
2025-09-29 12:56:02 +03:00
5fa5d97608 Logging tweak
Logging level is now fetched from config.py and can be adjusted in .env
file
2025-08-28 20:07:32 +03:00
9379aaeef6 Fix
Changed minioPopulator.py to ask required data though std::in instead of
getting it from config
2025-08-28 19:58:48 +03:00
e6537b2817 Bot and DB tweaks
Changes:
	General:
		- Implemented logging(loguru) throughout back and front
		  ends(primarily error handlers)
	Frontend:
		- Implemented setting commands for telegram bot command
		  menu
	Backend:
		- A schema and a table for bot are created in PGSQL DB
		  if there are no any
2025-08-28 19:43:37 +03:00
f7bcb89afa Added check on existence of objects in bucket 2025-08-17 18:56:19 +03:00
1e4c70ccef Combined 2 docker compose files into 1 2025-08-17 18:33:44 +03:00
465ed5749b Merge remote-tracking branch 'origin/main' 2025-08-17 18:18:20 +03:00
22f433828e - Added a script that batch adds images to a bucket of choice
- Cleaned some junk strings
2025-08-17 18:17:40 +03:00
e65b417ccc Delete src/Backend/minioPopulator.py 2025-08-04 16:35:36 +03:00
9e96fd247f Update src/Backend/UserTable.sql 2025-08-04 16:35:04 +03:00
3692c0233d 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
2025-08-02 18:58:12 +03:00
76bf8f9f26 Merge branch 'main' of https://git.frik.su/n0one/CatBot 2025-07-31 16:55:53 +03:00
4094312c37 Saved prototype comments in code 2025-07-31 16:49:44 +03:00
f53783efd3 New commit 2.0:
- Added test file to gitignore
    - Created table of users with chat_ids(UserTable.sql)
    - Added check of empty table into get_last_id
    - Modified get_user to return a chat_id instead of username
    - Changed download function in ISwork to send URL instead
2025-07-31 16:45:25 +03:00
e37a05b45c - Implemented echo bot and ability to quickly modify bot commands
- reworked config to grab values from .env
- Added a requirements file(with modules needed for bot to function)
- Added a separate bot initialization file for simpler launching
2025-07-31 15:49:12 +03:00
cd0536c393 - Updated gitignore
- Added bucket_name into config file
2025-07-30 19:16:44 +03:00
ed4443b220 Merge branch 'main' of https://git.frik.su/n0one/CatBot 2025-07-28 15:27:11 +03:00
79a2f1d7e7 deleted dependency on '.jpeg' extension 2025-07-28 15:25:42 +03:00
3353379e8c Обновить src/Backend/DBwork.py 2025-07-27 20:43:07 +03:00
2529138c4e Merge branch 'main' of https://git.frik.su/n0one/CatBot 2025-07-27 20:40:24 +03:00
e35a82c0bc updated functions connected to images 2025-07-27 20:36:52 +03:00
17 changed files with 798 additions and 181 deletions

16
.dockerignore Normal file
View File

@ -0,0 +1,16 @@
#.dockerignore
# Gitea
.gitea
# Docker
.dockerignore
dockerfile
compose.yaml
compose.yml
# Git
.gitignore
*.md
example.env
minioPopulator.py

View File

@ -0,0 +1,48 @@
name: Build and Push Docker Image
on:
release:
types: [published]
env:
REGISTRY: git.frik.su
IMAGE_NAME: ${{ gitea.repository }}
jobs:
build-and-push:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Install Docker
run: curl -fsSL https://get.docker.com | sh
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Login to registry
uses: docker/login-action@v2
with:
registry: ${{ env.REGISTRY }}
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Extract Docker tags from release
id: meta
uses: docker/metadata-action@v4
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=ref,event=tag
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}}
- name: Build and push Docker image
uses: docker/build-push-action@v4
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}

179
.gitignore vendored
View File

@ -1,5 +1,178 @@
.venv/
.idea/
# ---> Python
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
.pybuilder/
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# UV
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
#uv.lock
# poetry
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
#poetry.lock
# pdm
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
#pdm.lock
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
# in version control.
# https://pdm.fming.dev/latest/usage/project/#working-with-version-control
.pdm.toml
.pdm-python
.pdm-build/
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
src/.DS_Store
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
# Cython debug symbols
cython_debug/
# PyCharm
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/
# Ruff stuff:
.ruff_cache/
# PyPI configuration file
.pypirc
# Other
test.py

View File

@ -2,19 +2,22 @@
![Last Commit](https://img.shields.io/gitea/last-commit/n0one/CatBot?gitea_url=https%3A%2F%2Fgit.frik.su&color=FFCC00)
This is a simple (yet-to-be-made) telegram bot that sends photos of cats to a user, with an option to subscribe for daily cat photos. User will also be able to configure subscription cat pictures quantity.
This is a simple telegram bot that sends photos of cats to a user, with an option to subscribe for daily cat photos. User also is able to configure subscription cat pictures quantity.
## Authors
- [Ilya](https://git.frik.su/n0one)
- [Grisha](https://git.frik.su/zeroGRMh)
- [Zhenya](https://git.frik.su/EugeneBee)
### Bot uses:
- MinIO object storage
- PostgreSQL database
- Telegram API
- Docker container management platform
## Roadmap
- API keys configuration
- Frontend
- Backend
- DB configuration
- Documentation
- Licensing
### Setup:
- Get your telegram API bot token
- Copy compose.yml file and change environment variables as you need
- `docker compose up`
- Enjoy your own instance of the bot!
### Uploading pictures to MinIO:
For more comfortable pictures uploading you can use `minioPopulator.py` script.

70
compose.yml Normal file
View File

@ -0,0 +1,70 @@
version: '3.9'
services:
catbot:
image: git.frik.su/n0one/catbot:latest
container_name: catbot-bot
environment:
TG_TOKEN: your_telegram_token
IS_address: minio:9000
MINIO_ACCESS_KEY: minio
MINIO_SECRET_KEY: minio
MINIO_ROOT_USER: minio
BUCKET_NAME: catbot
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: postgres
LOGGING_LEVEL: info
depends_on:
postgres:
condition: service_healthy
minio:
condition: service_healthy
restart: unless-stopped
postgres:
image: postgres:latest
container_name: catbot-postgres
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: postgres
PGDATA: /var/lib/postgresql/data/pgdata
ports:
- "5432:5432"
volumes:
- ./pgdata:/var/lib/postgresql/data/pgdata
healthcheck:
test: ["CMD", "pg_isready", "-U", "postgres"]
interval: 30s
timeout: 20s
retries: 5
restart: unless-stopped
tty: true
stdin_open: true
minio:
image: minio/minio:latest
container_name: catbot-minio
ports:
- "9000:9000"
- "9001:9001"
environment:
MINIO_ACCESS_KEY: minio
MINIO_SECRET_KEY: minio
MINIO_ROOT_USER: minio
MINIO_ROOT_PASSWORD: minio
MINIO_BROWSER: "on"
volumes:
- ./data:/data
command: server /data --console-address ":9001"
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"]
interval: 30s
timeout: 20s
retries: 5
restart: unless-stopped
volumes:
pgdata:
driver: local

13
dockerfile Normal file
View File

@ -0,0 +1,13 @@
FROM python:3.13-slim
WORKDIR /app
RUN apt-get update && apt-get install -y --no-install-recommends libpq-dev build-essential
COPY requirements.txt ./
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
CMD ["python", "src/main.py"]

97
minioPopulator.py Normal file
View File

@ -0,0 +1,97 @@
import glob
import os
from minio import Minio
from minio.error import S3Error
def upload_image(bucket_name, object_name, file_path):
try:
if file_path.split('.')[-1] == 'jpeg':
client.fput_object(bucket_name, object_name, file_path, 'image/jpeg')
print(f'image {object_name} with local path \'{file_path}\' was successfully uploaded')
elif file_path.split('.')[-1] == 'jpg':
client.fput_object(bucket_name, object_name, file_path, 'image/jpg')
print(f'image {object_name} with local path \'{file_path}\' was successfully uploaded')
elif file_path.split('.')[-1] == 'png':
client.fput_object(bucket_name, object_name, file_path, 'image/png')
print(f'image {object_name} with local path \'{file_path}\' was successfully uploaded')
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 as e:
print(f'Error during MinIO operation: {e}')
# Main
print('Please enter your MinIO instance cloud address:')
IS_address = input()
print('Please enter MinIO access key:')
access_key = input()
print('Please enter MinIO secret key:')
secret_key = input()
print('Please enter bucket name:')
bucket = input()
client = Minio(
IS_address,
access_key=access_key,
secret_key=secret_key,
secure=False
)
is_found = client.bucket_exists(bucket)
if not is_found:
client.make_bucket(bucket)
print(f"Bucket '{bucket}' does not exist. A new bucket with that name have been created.")
else:
print(f"Bucket '{bucket}' exists. Proceeding...")
script_path = os.path.abspath(__file__).replace('\\', '/')
for i in range(len(script_path) - 1, 0, -1):
if script_path[i] == '/':
script_path = script_path[:i + 1]
break
paths = glob.glob(script_path + '*')
inter_paths, shortened_paths = [], []
for i in range(len(paths)):
paths[i] = os.path.abspath(paths[i]).replace('\\', '/')
for index, path in enumerate(paths, 0):
for j in range(len(path) - 1, 0, -1):
if path[j] == '/':
inter_paths.append(paths[index][j:])
for path in inter_paths:
if path.count('/') == 1 and (path[-3:] == 'jpg' or path[-3:] == 'png' or path[-4:] == 'jpeg' or path[-5:] == 'pjpeg'):
shortened_paths.append(path)
c = 0
for i in range(len(paths)):
i -= c
if not('.jpg' in paths[i] or '.jpeg' in paths[i] or '.pjpeg' in paths[i] or '.png' in paths[i]):
del paths[i]
c += 1
if len(paths) < i:
break
starting_weekday = 0
prev_obj_len = -1
for n in range(1, 8):
objects = client.list_objects(bucket, prefix=str(n) + '/')
obj_len = sum(1 for _ in objects)
if n == 1:
if obj_len < sum(1 for _ in client.list_objects(bucket, prefix='7/')):
starting_weekday = n - 1
elif prev_obj_len > obj_len:
starting_weekday = n - 1
break
prev_obj_len = obj_len
for i in range(0, len(shortened_paths)):
weekday = (i + starting_weekday) % 7 + 1
upload_image(bucket, str(weekday) + shortened_paths[i], paths[i])

Binary file not shown.

113
src/Backend/DBwork.py Normal file
View File

@ -0,0 +1,113 @@
import logging
import psycopg2
from loguru import logger
from src import config
logging_level = config.logging_level
logger.add(
"sys.stdout",
format="{time:YYYY-MM-DD HH:mm:ss.SSS} | {level} | {file}:{line} - {message}",
colorize=True,
level=logging_level
)
def get_last_id(cursor):
cursor.execute("SELECT MAX(id) FROM Users")
id = cursor.fetchall()[0][0]
if id is None:
return 0
return id
def set_connection():
try:
connection = psycopg2.connect(
dbname = config.db_name,
user = config.postgres_user,
password = config.postgres_password,
host = config.host_name,
port = config.port
)
cursor = connection.cursor()
logger.info('Successfully set connection to the PostgreSQL DB')
return cursor, connection
except psycopg2.Error as e:
logger.error(f'Failed to set connection to the PostgreSQL DB: {e.pgerror}')
raise e
def close_connection(connection, cursor):
try:
cursor.close()
connection.close()
except psycopg2.Error as e:
logger.error(f'Failed to close PostgreSQL connection: {e.pgerror}')
#Functions don't close connection automatically, it has to be closed manually
def add_user(chat_id, connection, cursor):
cursor.execute("INSERT INTO Users VALUES (%s, %s, %s);", (get_last_id(cursor) + 1, chat_id, 1))
connection.commit()
def delete_user(chat_id, connection, cursor):
cursor.execute("DELETE FROM Users WHERE chat_id = %s;", (chat_id,))
connection.commit()
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()
def get_images_amount(chat_id, 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):
cursor.execute("SELECT chat_id FROM Users WHERE id = %s", (id,))
chat_id = cursor.fetchall()[0][0]
return chat_id
def schema_creator(schema_name):
cur, conn = set_connection()
try:
cur.execute(f'CREATE SCHEMA IF NOT EXISTS {schema_name};')
conn.commit()
logger.info(f'Successfully created schema {schema_name} if it didn\'t exist yet')
except psycopg2.Error as e:
logger.error(f'Error during schema creation: {e}')
finally:
close_connection(conn, cur)
def table_creator(schema_name, table_name):
cur, conn = set_connection()
try:
cur.execute(f'''
CREATE TABLE IF NOT EXISTS {schema_name}.{table_name}
(
id integer NOT NULL DEFAULT nextval('users_id_seq'::regclass),
chat_id bigint NOT NULL,
images_amount bigint,
CONSTRAINT users_pkey PRIMARY KEY (id),
CONSTRAINT chat_id_unique UNIQUE (chat_id)
)
TABLESPACE pg_default;
ALTER TABLE IF EXISTS {schema_name}.users
OWNER to {config.postgres_user};
''')
conn.commit()
logger.info(f'Successfully created table {table_name} in schema {schema_name} if it didn\'t exist yet')
except psycopg2.Error as e:
logging.error(f'Error during table creation: {e}')
finally:
close_connection(conn, cur)

View File

@ -1,36 +1,78 @@
from minio import Minio
from datetime import timedelta
from random import randint
import DBwork
from loguru import logger
from minio import Minio, S3Error
from src import config
logging_level = config.logging_level
logger.add(
"sys.stdout",
format="{time:YYYY-MM-DD HH:mm:ss.SSS} | {level} | {file}:{line} - {message}",
colorize=True,
level=logging_level
)
def _setClient():
minio_client = Minio(
config.IS_address,
access_key = config.acc_key,
secret_key = config.sec_key,
secure = False
)
return minio_client
try:
minio_client = Minio(
config.IS_address, # type: ignore
access_key = config.acc_key, # type: ignore
secret_key = config.sec_key, # type: ignore
secure = False
)
logger.info('Successfully set connection to the MinIO bucket')
return minio_client
except S3Error as e:
logger.error(f'S3 error during connection to bucket. Code: {e.code}, Message: {e.message}')
def getImageName(currentDay):
maxFiles = 2
def getNumberofObjects(client, currentDay):
objects = client.list_objects(config.bucket_name, prefix=str(currentDay) + '/')
numberOfObjects = sum(1 for _ in objects)
return numberOfObjects
def getObjectExtension(client, currentDay, fileNumber):
objects = client.list_objects(config.bucket_name, prefix=str(currentDay) + '/')
counter = 0
object_extension = None
for obj in objects:
counter += 1
if counter == fileNumber:
object_extension = obj.object_name.split('.')[-1]
return object_extension
def getImageName(currentDay, client):
maxFiles = getNumberofObjects(client, currentDay)
if maxFiles == 0:
return None
fileNumber = randint(1, maxFiles)
desiredFile = currentDay + '/' + str(fileNumber) + '.jpeg'
shortExtension = getObjectExtension(client, currentDay, fileNumber)
fileExtension = ''
if shortExtension is not None:
fileExtension = '.' + shortExtension
desiredFile = str(currentDay) + '/' + str(fileNumber) + fileExtension
return desiredFile
def downloadImage(currentDay, username):
bucket_name = "cat-images"
def getDownloadURL(currentDay):
client = _setClient()
client.fget_object(bucket_name, getImageName(currentDay), username + '.jpeg')
if client is None:
logger.error("Failed to set MinIO client")
return None
def downloadForAll(currentDay):
cur, conn = DBwork.set_connection()
counter = 1
user = DBwork.get_user(counter, cur)
while(user != 'Error'):
downloadImage(currentDay, user)
counter += 1
user = DBwork.get_user(counter, cur)
DBwork.close_connection(conn, cur)
object_name = getImageName(currentDay, client)
if object_name is None:
logger.error(f"Can't generate a URL: no files in current MinIO directory({currentDay})")
return None
url = client.presigned_get_object(
config.bucket_name, # type: ignore
object_name,
expires=timedelta(days=1)
)
return url

View File

@ -1,48 +0,0 @@
import psycopg2
from src import config
def _get_last_id(cursor):
cursor.execute("SELECT MAX(id) FROM usernames")
id = cursor.fetchall()[0][0]
return id
def set_connection():
connection = psycopg2.connect(
dbname = config.db_name,
user = config.postgres_user,
password = config.postgres_password,
host = config.host_name,
port = config.port
)
cursor = connection.cursor()
return cursor, connection
def close_connection(connection, cursor):
cursor.close()
connection.close()
#Functions don't close connection automatically, it has to be closed manually
def add_user(username, connection, cursor):
cursor = connection.cursor()
cursor.execute("INSERT INTO usernames VALUES (%s, %s);", (_get_last_id(cursor) + 1, username))
connection.commit()
def delete_user(username, connection, cursor):
cursor = connection.cursor()
cursor.execute("DELETE FROM usernames WHERE username = %s;", (username,))
connection.commit()
def change_name(old_username, new_username, connection, cursor):
cursor = connection.cursor()
cursor.execute("UPDATE usernames SET username = %s WHERE username = %s;", (new_username, old_username))
connection.commit()
def get_user(id, cursor):
max_id = _get_last_id(cursor)
if id <= max_id:
cursor.execute("SELECT username FROM usernames WHERE id = %s", (id,))
username = cursor.fetchall()[0][0]
else:
username = 'Error'
return username, cursor

View File

@ -1,43 +0,0 @@
version: '3.9'
services:
postgres:
image: postgres:latest
container_name: postgres_container
environment:
POSTGRES_USER: postgres_user
POSTGRES_PASSWORD: postgres_password
POSTGRES_DB: postgres_db
PGDATA: /var/lib/postgresql/data/pgdata
ports:
- "5430:5432"
volumes:
- ./pgdata:/var/lib/postgresql/data/pgdata
deploy:
resources:
limits:
cpus: '0.50'
memory: 512M
reservations:
cpus: '0.25'
memory: 256M
command: >
postgres -c max_connections=1000
-c shared_buffers=256MB
-c effective_cache_size=768MB
-c maintenance_work_mem=64MB
-c checkpoint_completion_target=0.7
-c wal_buffers=16MB
-c default_statistics_target=100
healthcheck:
test: [ "CMD-SHELL", "pg_isready -U postgres_user -d postgres_db" ]
interval: 30s
timeout: 10s
retries: 5
restart: unless-stopped
tty: true
stdin_open: true
volumes:
pgdata:
driver: local

View File

@ -1,22 +0,0 @@
version: "3.9"
services:
minio:
image: minio/minio:latest
ports:
- "9000:9000"
- "9001:9001"
environment:
MINIO_ACCESS_KEY: minioadmin
MINIO_SECRET_KEY: minioadmin
MINIO_ROOT_USER: minioadmin
MINIO_ROOT_PASSWORD: minioadmin
MINIO_BROWSER: "on"
volumes:
- ./data:/data
command: server /data --console-address ":9001"
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"]
interval: 30s
timeout: 20s
retries: 3
restart: unless-stopped

View File

@ -1,37 +1,191 @@
from src import config
import asyncio
import logging
from aiogram import Bot, Dispatcher, F, Router
from aiogram.filters import Command
from aiogram.types import Message
from datetime import datetime
import psycopg2
from aiogram import Bot, Dispatcher
from aiogram.client.default import DefaultBotProperties
from aiogram.enums import ParseMode
from aiogram.filters import Command, CommandObject
from aiogram.fsm.storage.memory import MemoryStorage
from aiogram.types import (
BotCommand,
BotCommandScopeAllGroupChats,
BotCommandScopeAllPrivateChats,
Message,
URLInputFile,
)
from apscheduler.schedulers.asyncio import AsyncIOScheduler
from loguru import logger
from src import config
from src.Backend import DBwork, ISwork
current_day = datetime.now().weekday()
start_router = Router()
scheduler = AsyncIOScheduler(timezone = 'Europe/Moscow')
logging.basicConfig(level = logging.INFO, format = '%(asctime)s - %(name)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)
logging_level = config.logging_level
logger.add(
"sys.stdout",
format="{time:YYYY-MM-DD HH:mm:ss.SSS} | {level} | {file}:{line} - {message}",
colorize=True,
level=logging_level
)
bot = Bot(token = config.TG_TOKEN , default = DefaultBotProperties(parse_mode = ParseMode.HTML))
bot = Bot(token = config.TG_token , default = DefaultBotProperties(parse_mode = ParseMode.HTML)) # type: ignore
dp = Dispatcher(storage = MemoryStorage())
schema_name = 'catbot'
table_name = 'Users'
@dp.message(Command('start'))
async def cmd_start(message: Message):
await message.answer('Запуск сообщения по команде /start используя фильтр CommandStart()')
await message.answer(
text='''
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)
''',
parse_mode=None
)
logger.info(f'Command /start executed successfully. ChatID: {message.chat.id}')
@dp.message(F.text)
async def basic_reaction(message: Message):
if message.text != '':
await message.answer(message.text)
@dp.message(Command('cat'))
async def cmd_cat(message: Message):
chat_id = message.chat.id
download_url = ISwork.getDownloadURL(current_day)
if download_url is not None:
image_link = URLInputFile(
download_url,
filename=datetime.now().strftime('%Y_%m_%d_%H_%M_%S')
)
else:
image_link = None
if image_link is None:
await bot.send_message(chat_id=chat_id, text='We are sorry, but there seems to be a problem with finding images for today.')
else:
await message.answer_photo(image_link, caption='Look, a cat :3')
logger.info(f'Command /cat executed successfully. ChatID: {chat_id}')
@dp.message(Command('subscription_modify'))
async def subscription_modify(message: Message, command: CommandObject):
chat_id = message.chat.id
if command.args is None or not command.args.isdigit():
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() # type: ignore
amount = command.args
DBwork.change_images_amount(chat_id, amount, connection, cursor)
DBwork.close_connection(connection, cursor)
except psycopg2.Error:
if psycopg2.errors.IntegrityError:
await message.answer('You are not yet subscribed.')
logger.warning(f'A non-subscribed user in chat {chat_id} tried to modify subscription')
else:
await message.answer('There seems to be a problem on our side.')
return
await message.answer('Amount of daily images was changed successfully!')
logger.info(f'Command /subscription_modify executed successfully. ChatID: {chat_id}')
@dp.message(Command('subscribe'))
async def cmd_subscribe(message: Message):
chat_id = message.chat.id
try:
cursor, connection = DBwork.set_connection()
DBwork.add_user(chat_id, connection, cursor)
DBwork.close_connection(connection, cursor)
except psycopg2.Error as e:
if psycopg2.errors.UniqueViolation:
await message.answer('You are already subscribed.')
logger.warning(f'An already subscribed user in chat {chat_id} tried to subscribe again')
else:
await message.answer('There seems to be a problem on our side.')
logger.error(f'PostgreSQL error occurred. ChatID: {chat_id}. Error: {str(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.
''')
logger.info(f'Command /subscribe executed successfully. ChatID: {chat_id}')
@dp.message(Command('unsubscribe'))
async def cmd_unsubscribe(message: Message):
chat_id = message.chat.id
try:
cursor, connection = DBwork.set_connection()
DBwork.delete_user(chat_id, connection, cursor)
DBwork.close_connection(connection, cursor)
except psycopg2.Error as e:
if psycopg2.errors.NoData:
await message.answer('You are not yet subscribed.')
logger.warning(f'A non-subscribed user in chat {chat_id} tried to unsubscribe')
else:
await message.answer('There seems to be a problem on our side.')
logger.error(f'PostgreSQL error occurred. ChatID: {chat_id}. Error: {str(e.pgerror)}')
return
await message.answer('You have successfully unsubscribed.')
logger.info(f'Command /unsubscribe executed successfully. ChatID: {chat_id}')
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, cursor)
for _ in range(images_amount):
url = ISwork.getDownloadURL(current_day)
if url is not None:
image_link = URLInputFile(url, filename=datetime.now().strftime('%Y_%m_%d_%H_%M_%S'))
else:
image_link = None
if image_link is None:
await bot.send_message(chat_id=chat_id, text="We are sorry, but there seems to be a problem with finding images for today.")
else:
await bot.send_photo(chat_id = chat_id, photo = image_link)
DBwork.close_connection(connection, cursor)
logger.info('Daily mass sending to subscribers has finished')
async def set_commands_for_menu():
commands = [
BotCommand(command='start', description='Get info about the bot and its commands'),
BotCommand(command='cat', description='Request an image of a cat from today\'s pool of images'),
BotCommand(command='subscribe', description='Subscribe to daily cat images sent at 12:00 UTC+3'),
BotCommand(command='subscription_modify', description='Change the amount of images sent daily'),
BotCommand(command='unsubscribe', description='Cancel your subscription')
]
await bot.set_my_commands(commands=commands, scope=BotCommandScopeAllPrivateChats())
await bot.set_my_commands(commands=commands, scope=BotCommandScopeAllGroupChats())
logger.info('Command menu for bot has been set')
scheduler.add_job(send_daily_images, 'cron', hour = 12, minute = 0)
async def main():
DBwork.schema_creator(schema_name)
DBwork.table_creator(schema_name, table_name)
await set_commands_for_menu()
scheduler.start()
await dp.start_polling(bot)

View File

@ -1 +0,0 @@
Here should be frontend (commands interactions)

View File

@ -1,5 +1,5 @@
import asyncio
from Frontend.createbot import main
asyncio.run(main())

View File

@ -1,15 +1,17 @@
from decouple import config
TG_token = config('TG_TOKEN')
IS_address = config('IS_address')
acc_key = config('acc_key')
sec_key = config('sec_key')
db_name = config('db_name')
postgres_user = config('postgres_user')
postgres_password = config('postgres_password')
host_name = config('host_name')
port = config('port')
IS_address = config('IS_ADDRESS')
acc_key = config('MINIO_ACCESS_KEY')
sec_key = config('MINIO_SECRET_KEY')
root_user = config('MINIO_ROOT_USER')
bucket_name = config('BUCKET_NAME')
db_name = config('DB_NAME')
postgres_user = config('POSTGRES_USER')
postgres_password = config('POSTGRES_PASSWORD')
host_name = config('POSTGRES_HOST_NAME')
port = config('POSTGRES_PORT')
TG_TOKEN = config('TG_TOKEN')
# ADMINS = [int(admin_id) for admin_id in config('ADMINS').split(',')]
logging_level = config('LOGGING_LEVEL')