Compare commits
28 Commits
4ed1dc9d31
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| cb0731feba | |||
| 5fa5d97608 | |||
| 9379aaeef6 | |||
| e6537b2817 | |||
| f7bcb89afa | |||
| 1e4c70ccef | |||
| 465ed5749b | |||
| 22f433828e | |||
| e65b417ccc | |||
| 9e96fd247f | |||
| 3692c0233d | |||
| 76bf8f9f26 | |||
| 4094312c37 | |||
| f53783efd3 | |||
| e37a05b45c | |||
| cd0536c393 | |||
| ed4443b220 | |||
| 79a2f1d7e7 | |||
| 3353379e8c | |||
| 2529138c4e | |||
| e35a82c0bc | |||
| 977d25538d | |||
| fdcad46ace | |||
| 6943bd3b24 | |||
| fb7d0412e9 | |||
| f5fe578a88 | |||
| 1e024cc937 | |||
| 656e9257cc |
16
.dockerignore
Normal file
16
.dockerignore
Normal file
@ -0,0 +1,16 @@
|
||||
#.dockerignore
|
||||
# Gitea
|
||||
.gitea
|
||||
|
||||
# Docker
|
||||
.dockerignore
|
||||
dockerfile
|
||||
compose.yaml
|
||||
compose.yml
|
||||
|
||||
# Git
|
||||
.gitignore
|
||||
*.md
|
||||
example.env
|
||||
|
||||
minioPopulator.py
|
||||
48
.gitea/workflows/docker-build-push.yaml
Normal file
48
.gitea/workflows/docker-build-push.yaml
Normal 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 }}
|
||||
178
.gitignore
vendored
178
.gitignore
vendored
@ -1,4 +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
|
||||
.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
|
||||
|
||||
27
README.md
27
README.md
@ -2,19 +2,22 @@
|
||||
|
||||

|
||||
|
||||
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
70
compose.yml
Normal 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
13
dockerfile
Normal 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
97
minioPopulator.py
Normal 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])
|
||||
BIN
requirements.txt
Normal file
BIN
requirements.txt
Normal file
Binary file not shown.
BIN
src/.DS_Store
vendored
BIN
src/.DS_Store
vendored
Binary file not shown.
113
src/Backend/DBwork.py
Normal file
113
src/Backend/DBwork.py
Normal 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)
|
||||
@ -1,34 +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():
|
||||
try:
|
||||
minio_client = Minio(
|
||||
"localhost:9000",
|
||||
access_key="minioadmin",
|
||||
secret_key="minioadmin",
|
||||
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):
|
||||
counter = 1
|
||||
user = dbwork.get_user(counter)
|
||||
while(user != 'Error'):
|
||||
downloadImage(currentDay, user)
|
||||
counter += 1
|
||||
user = dbwork.get_user(counter)
|
||||
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
|
||||
|
||||
Day = 'Tuesday'
|
||||
downloadForAll(Day)
|
||||
url = client.presigned_get_object(
|
||||
config.bucket_name, # type: ignore
|
||||
object_name,
|
||||
expires=timedelta(days=1)
|
||||
)
|
||||
return url
|
||||
|
||||
@ -1,67 +0,0 @@
|
||||
import psycopg2
|
||||
|
||||
def _get_last_id(cursor):
|
||||
cursor.execute("SELECT MAX(id) FROM usernames")
|
||||
id = cursor.fetchall()[0][0]
|
||||
return id
|
||||
|
||||
def add_user(username):
|
||||
connection = psycopg2.connect(
|
||||
dbname="postgres_db",
|
||||
user="postgres_user",
|
||||
password="postgres_password",
|
||||
host="localhost",
|
||||
port="5430"
|
||||
)
|
||||
cursor = connection.cursor()
|
||||
cursor.execute("INSERT INTO usernames VALUES (%s, %s);", (_get_last_id(cursor) + 1, username))
|
||||
connection.commit()
|
||||
cursor.close()
|
||||
connection.close()
|
||||
|
||||
def delete_user(username):
|
||||
connection = psycopg2.connect(
|
||||
dbname="postgres_db",
|
||||
user="postgres_user",
|
||||
password="postgres_password",
|
||||
host="localhost",
|
||||
port="5430"
|
||||
)
|
||||
cursor = connection.cursor()
|
||||
cursor.execute("DELETE FROM usernames WHERE username = %s;", (username,))
|
||||
connection.commit()
|
||||
cursor.close()
|
||||
connection.close()
|
||||
|
||||
def change_name(old_username, new_username):
|
||||
connection = psycopg2.connect(
|
||||
dbname="postgres_db",
|
||||
user="postgres_user",
|
||||
password="postgres_password",
|
||||
host="localhost",
|
||||
port="5430"
|
||||
)
|
||||
cursor = connection.cursor()
|
||||
cursor.execute("UPDATE usernames SET username = %s WHERE username = %s;", (new_username, old_username))
|
||||
connection.commit()
|
||||
cursor.close()
|
||||
connection.close()
|
||||
|
||||
def get_user(id):
|
||||
connection = psycopg2.connect(
|
||||
dbname="postgres_db",
|
||||
user="postgres_user",
|
||||
password="postgres_password",
|
||||
host="localhost",
|
||||
port="5430"
|
||||
)
|
||||
cursor = connection.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'
|
||||
cursor.close()
|
||||
connection.close()
|
||||
return username
|
||||
@ -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
|
||||
@ -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
|
||||
BIN
src/Frontend/.DS_Store
vendored
BIN
src/Frontend/.DS_Store
vendored
Binary file not shown.
191
src/Frontend/createbot.py
Normal file
191
src/Frontend/createbot.py
Normal file
@ -0,0 +1,191 @@
|
||||
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()
|
||||
|
||||
scheduler = AsyncIOScheduler(timezone = 'Europe/Moscow')
|
||||
|
||||
|
||||
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)) # 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(
|
||||
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(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)
|
||||
@ -1 +0,0 @@
|
||||
Here should be frontend (commands interactions)
|
||||
5
src/__main__.py
Normal file
5
src/__main__.py
Normal file
@ -0,0 +1,5 @@
|
||||
import asyncio
|
||||
|
||||
from Frontend.createbot import main
|
||||
|
||||
asyncio.run(main())
|
||||
17
src/config.py
Normal file
17
src/config.py
Normal file
@ -0,0 +1,17 @@
|
||||
from decouple import config
|
||||
|
||||
TG_token = config('TG_TOKEN')
|
||||
|
||||
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')
|
||||
|
||||
logging_level = config('LOGGING_LEVEL')
|
||||
Reference in New Issue
Block a user