firt commit
This commit is contained in:
commit
b8ab8b4b93
|
|
@ -0,0 +1,29 @@
|
||||||
|
BASE_URL=https://basa.houshan.ai
|
||||||
|
DATABASE_URL=postgres://root:WwLbnHzA0fC15QzKSORSvsXr@fitz-roy.liara.cloud:33568/houshan
|
||||||
|
JWT_SECRET_KEY=g,m`oF]Bs$pf)afA[2J*Dsb*zKc|o$eQ
|
||||||
|
JWT_ADMIN_SECRET_KEY=XRU@4xfZS&egA&&93,o@oVtONr>9Ur=4lI%5U[ize{<!?:~AE~E}!9(S%F
|
||||||
|
JWT_ALGORITHM=HS256
|
||||||
|
KAVENEGAR_API_KEY=346446725341592F2F4554514846714A366A6349327A376671316779412F63474F48684D6348372B326A553D
|
||||||
|
OPENAI_API_KEY=sk-svcacct-ANnKlsmi8reyhCDabpNkw91XG2GeKI_A3qgXIZNisyUshaJsRaCXxROysU8RbdK5rFBwrVcvUT3BlbkFJkIEQOrGmbyqplmAWPM3QpAFHwdmy7DmPe-U2Hi-tuslb1bCVoZP0dIUg-_1G735Pbk9-bXuYgA
|
||||||
|
GOOGLE_API_KEY=AIzaSyCzoZLhdyCBYM9EgZTCtmiQpco1zf6HTYY
|
||||||
|
ST_ENDPOINT=https://storage.iran.liara.space
|
||||||
|
ST_ACCESS_KEY=ac027l63bi919muk
|
||||||
|
ST_SECRET_KEY=d197e392-a500-4808-bb4e-4ed885a575db
|
||||||
|
ST_BUCKET_NAME=hoshan
|
||||||
|
YDC_API_KEY=c434ddd9-77dd-4e90-816f-8c7d3fda484a<__>1QIPqdETU8N2v5f4q61PbN6H
|
||||||
|
CHROMA_URL=https://houshan-chroma.liara.run
|
||||||
|
CHROMA_CREDENTIALS=RkjmBFhVBlnUIweJGEB
|
||||||
|
ANTHROPIC_API_KEY=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
|
||||||
|
MMERCHANT_ID=667136b5-b23e-45fb-844f-4d2c7fd3fef3
|
||||||
|
CAFEBAZAAR_PISHKHAN_API_SECRET=eyJhbGciOiJIUzI1NiIsImtpZCI6ImFuY2llbnQiLCJ0eXAiOiJKV1QifQ.eyJpc3MiOiJuYXNoZXItcGlzaGtoYW4tYXBpIiwiaWF0IjoxNzM3MTk5MjE0LCJleHAiOjQ4OTA3OTkyMTQsImFwaV9hZ2VudF9pZCI6MzM2MX0.Tj-eTmAAN4hZ8xAaeiJoRnoC3uYdWYqieX2OATjZLOM
|
||||||
|
CAFEBAZAAR_PRICE_KEY=6X0QCJnxK3CqFB9dFlwFbqpZhK4SOqKG1gxXhdxWNSg
|
||||||
|
MYKET_ACCESS_TOKEN=37fa0df1-5a6e-46b3-8b84-b0da43f9749c
|
||||||
|
DEEPSEEK_API_KEY=sk-a89054e15dbf4f21a3715470db084765
|
||||||
|
GHOST_ID=6b449ccb-bd7b-4988-986a-35e653569785
|
||||||
|
FAL_KEY=3a793a2f-65ac-404a-aca3-02d36a7d2cb0:77c185cd1adfc8593e6ad8343b4f28d9
|
||||||
|
MAILERSEND_API_KEY=mlsn.eebd4e124869da92ca8a83bd876c3a8e57dd73e9dae61c5c3bf77e17546e56df
|
||||||
|
SUNO_API_KEY=39f3326be8f930953a7a0b80a53b7231
|
||||||
|
PIXVERSE_API_KEY=sk-2c785f1f77ace5b4f39cb3d4dc5ef554
|
||||||
|
INTERNAL_TELEGRAM_SECRET=do8asyd0h21uodh2od2hkdmbzxc2349ASAX80scasokcu23ked2zxc
|
||||||
|
LIARA_API_URL =https://ai.liara.ir/api/68eb653bb55873971e0d46d6/v1
|
||||||
|
LIARA_API_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJrZXkiOiI2OThhZGIxMWQzODZhNWVmODNlMTg2YzAiLCJ0eXBlIjoiYWlfa2V5IiwiaWF0IjoxNzcwNzA3NzMwfQ.ckoR00Uxt8W4DmLCGW24P46Zq-et0Yhtu2xej5P0ClQ
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
.venv
|
||||||
|
.env
|
||||||
|
db-backup
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
HOSTNAME=basa.houshan.ai
|
||||||
|
|
||||||
|
API_HOSTNAME=basa.houshan.ai
|
||||||
|
API_DB_BACKUP_DELAY=120s
|
||||||
|
API_DB_BACKUP_INTERVAL=86400s
|
||||||
|
API_DB_NAME=houshan
|
||||||
|
API_DB_USER=root
|
||||||
|
API_DB_PASS=lXU4kz2JSsJKM6PiBSbVddyE
|
||||||
|
|
||||||
|
# Traefik
|
||||||
|
TRAEFIK_IMAGE_TAG=traefik:v3.3.4nhvu
|
||||||
|
TRAEFIK_HOSTNAME=traefik.houshan.ai
|
||||||
|
TRAEFIK_LOG_LEVEL=INFO
|
||||||
|
TRAEFIK_LOG_FORMAT=json
|
||||||
|
TRAEFIK_ACCESS_LOG_FORMAT=json
|
||||||
|
TRAEFIK_BASIC_AUTH=admin:$$apr1$$m7z6vCld$$EXqonhrD4jI05jKZLD7Gh1 # htpasswd -nb user pass
|
||||||
|
TRAEFIK_METRICS_PORT=8899
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
# fluter
|
||||||
|
build.zip
|
||||||
|
|
||||||
|
# Virtual environment
|
||||||
|
env/
|
||||||
|
.idea
|
||||||
|
|
||||||
|
# Environment variables
|
||||||
|
# .env
|
||||||
|
# .api.env
|
||||||
|
db-backup
|
||||||
|
|
||||||
|
# Python compiled files
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
|
||||||
|
#
|
||||||
|
serviceAccountKey.json
|
||||||
|
|
@ -0,0 +1,44 @@
|
||||||
|
# Use an official Python runtime as a base image
|
||||||
|
FROM python:3.11-slim
|
||||||
|
|
||||||
|
# Set the working directory inside the container
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Install system dependencies
|
||||||
|
RUN apt-get update && apt-get install -y \
|
||||||
|
curl \
|
||||||
|
libpq-dev \
|
||||||
|
gcc \
|
||||||
|
ffmpeg \
|
||||||
|
&& apt-get clean \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# --- تنظیمات ریجستری پایتون (Runflare) ---
|
||||||
|
# روش پیشنهادی استفاده از ENV است که روی تمام دستورات pip تاثیر میگذارد
|
||||||
|
ENV PIP_INDEX_URL=https://mirror-pypi.runflare.com/simple
|
||||||
|
ENV PIP_TRUSTED_HOST=mirror-pypi.runflare.com
|
||||||
|
|
||||||
|
# اگر اصرار دارید دقیقا از دستورات config استفاده کنید، به این صورت جایگزین کنید:
|
||||||
|
# RUN pip config set global.index-url https://mirror-pypi.runflare.com/simple && \
|
||||||
|
# pip config set global.trusted-host mirror-pypi.runflare.com
|
||||||
|
|
||||||
|
# نکته: خط مربوط به npm config حذف شد چون در این ایمیج npm وجود ندارد.
|
||||||
|
# اگر به Node نیاز دارید، باید ابتدا آن را نصب کنید.
|
||||||
|
|
||||||
|
# Copy requirements.txt to the container
|
||||||
|
COPY requirements.txt .
|
||||||
|
|
||||||
|
# Upgrade pip and install wheel using the mirror
|
||||||
|
RUN pip install --no-cache-dir --upgrade pip setuptools wheel
|
||||||
|
|
||||||
|
# Install the Python dependencies
|
||||||
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
|
|
||||||
|
# Copy the rest of the FastAPI app code into the container
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Expose port 8000
|
||||||
|
EXPOSE 8000
|
||||||
|
|
||||||
|
# Command to run FastAPI app
|
||||||
|
CMD ["uvicorn", "src.main:app", "--host", "0.0.0.0", "--port", "8000"]
|
||||||
|
|
@ -0,0 +1,83 @@
|
||||||
|
# Define variables for better maintainability
|
||||||
|
COMPOSE_FILE := docker-compose.yml
|
||||||
|
PROJECT_NAME := houshan # Replaced space with a hyphen
|
||||||
|
|
||||||
|
# Default target
|
||||||
|
all: up
|
||||||
|
|
||||||
|
## Build, (re)create, start, and attach to containers
|
||||||
|
up:
|
||||||
|
@echo "Bringing up the services..."
|
||||||
|
sudo docker compose -f $(COMPOSE_FILE) -p $(PROJECT_NAME) up --build --remove-orphans -d
|
||||||
|
|
||||||
|
## Stop and remove containers, networks, images, and volumes
|
||||||
|
down:
|
||||||
|
@echo "Stopping and removing the services..."
|
||||||
|
sudo docker compose -f $(COMPOSE_FILE) -p $(PROJECT_NAME) down --remove-orphans
|
||||||
|
|
||||||
|
## Rebuild and restart the services
|
||||||
|
rebuild:
|
||||||
|
@echo "Rebuilding the services..."
|
||||||
|
sudo docker compose -f $(COMPOSE_FILE) -p $(PROJECT_NAME) up --build --force-recreate --remove-orphans
|
||||||
|
|
||||||
|
## Stop the running containers (without removing them)
|
||||||
|
stop:
|
||||||
|
@echo "Stopping the running containers..."
|
||||||
|
sudo docker compose -f $(COMPOSE_FILE) -p $(PROJECT_NAME) stop
|
||||||
|
|
||||||
|
## Show the status of the containers
|
||||||
|
ps:
|
||||||
|
@echo "Displaying the status of the containers..."
|
||||||
|
sudo docker compose -f $(COMPOSE_FILE) -p $(PROJECT_NAME) ps
|
||||||
|
|
||||||
|
## View the recent logs of the running containers
|
||||||
|
logs:
|
||||||
|
@echo "Displaying the logs of the containers (use Ctrl+C to exit)..."
|
||||||
|
sudo docker compose -f $(COMPOSE_FILE) -p $(PROJECT_NAME) logs -f --since 0s
|
||||||
|
|
||||||
|
## View the logs of the running containers
|
||||||
|
logs-all:
|
||||||
|
@echo "Displaying the logs of the containers (use Ctrl+C to exit)..."
|
||||||
|
sudo docker compose -f $(COMPOSE_FILE) -p $(PROJECT_NAME) logs -f
|
||||||
|
|
||||||
|
|
||||||
|
## Clean up unused Docker resources (volumes, networks, images)
|
||||||
|
clean:
|
||||||
|
@echo "Cleaning up unused Docker resources..."
|
||||||
|
sudo docker system prune -af --volumes
|
||||||
|
|
||||||
|
## Get shell from app
|
||||||
|
shell-app:
|
||||||
|
@echo "Getting shell from app..."
|
||||||
|
sudo docker compose -f $(COMPOSE_FILE) -p $(PROJECT_NAME) exec -it app /bin/bash
|
||||||
|
|
||||||
|
## restore data
|
||||||
|
db-restore:
|
||||||
|
@echo "Restore pg_dump data to db..."
|
||||||
|
sudo docker compose -f $(COMPOSE_FILE) -p $(PROJECT_NAME) exec -i -T database pg_restore -U root -d houshan --verbose < ./restore/bots.sql
|
||||||
|
|
||||||
|
## Help message
|
||||||
|
help:
|
||||||
|
@echo "Available targets:"
|
||||||
|
@echo " up - Start the services (builds if necessary)"
|
||||||
|
@echo " down - Stop and remove the services"
|
||||||
|
@echo " rebuild - Rebuild and restart the services"
|
||||||
|
@echo " stop - Stop the running containers"
|
||||||
|
@echo " ps - Show the status of the containers"
|
||||||
|
@echo " logs - View the logs of the running containers (use -f to follow logs in real-time)"
|
||||||
|
@echo " clean - Clean up unused Docker resources"
|
||||||
|
|
||||||
|
.PHONY: all up down rebuild stop ps logs clean help
|
||||||
|
|
||||||
|
# db-init (don't):
|
||||||
|
# aerich init -t src.settings.TORTOISE_ORM
|
||||||
|
|
||||||
|
# db-migrate:
|
||||||
|
# aerich migrate --name ""
|
||||||
|
|
||||||
|
# db-upgrade:
|
||||||
|
# export DATABASE_URL=postgres://root:XXXXX@localhost:54921/houshan
|
||||||
|
# aerich upgrade
|
||||||
|
|
||||||
|
# db-copy:
|
||||||
|
# pg_dump -h himalayas.liara.cloud -p 30555 -U root -d houshan -F c -t bots | pv > bots.sql
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
#!/bin/sh
|
||||||
|
set -e
|
||||||
|
echo "Starting backups service"
|
||||||
|
sleep "${API_DB_BACKUP_DELAY}"
|
||||||
|
|
||||||
|
while true; do
|
||||||
|
NOW=$(date "+%Y-%m-%d_%H-%M")
|
||||||
|
FILE="/srv/houshan-postgres/backups/houshan-postgres-backup-${NOW}.backup"
|
||||||
|
|
||||||
|
echo "[${NOW}] Running pg_dump (custom format)..."
|
||||||
|
pg_dump -h postgres -p 5432 -d "${API_DB_NAME}" -U "${API_DB_USER}" -F c -f "${FILE}"
|
||||||
|
|
||||||
|
echo "[${NOW}] Backup saved to ${FILE}"
|
||||||
|
|
||||||
|
echo "[${NOW}] Keeping last 3 backups..."
|
||||||
|
ls -1t /srv/houshan-postgres/backups/houshan-postgres-backup-*.backup | tail -n +4 | xargs -r rm -f
|
||||||
|
|
||||||
|
echo "[${NOW}] Sleeping for ${API_DB_BACKUP_INTERVAL}"
|
||||||
|
sleep "${API_DB_BACKUP_INTERVAL}"
|
||||||
|
done
|
||||||
|
|
@ -0,0 +1,137 @@
|
||||||
|
networks:
|
||||||
|
houshan-network:
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
houshan-postgres:
|
||||||
|
|
||||||
|
services:
|
||||||
|
# ---------------------------------
|
||||||
|
# API Service (Your Python App)
|
||||||
|
# ---------------------------------
|
||||||
|
app:
|
||||||
|
build: .
|
||||||
|
container_name: basa_api
|
||||||
|
networks:
|
||||||
|
- houshan-network
|
||||||
|
env_file:
|
||||||
|
- .api.env
|
||||||
|
restart: always
|
||||||
|
labels:
|
||||||
|
- "autoheal=true"
|
||||||
|
healthcheck:
|
||||||
|
test:
|
||||||
|
[
|
||||||
|
"CMD",
|
||||||
|
"curl",
|
||||||
|
"--fail",
|
||||||
|
"--max-time",
|
||||||
|
"10",
|
||||||
|
"http://localhost:8000/category/",
|
||||||
|
]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 2
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------
|
||||||
|
# Autoheal Service
|
||||||
|
# ---------------------------------
|
||||||
|
autoheal:
|
||||||
|
image: willfarrell/autoheal
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
- AUTOHEAL_CONTAINER_LABEL=autoheal
|
||||||
|
- AUTOHEAL_DEFAULT_STOP_TIMEOUT=10
|
||||||
|
volumes:
|
||||||
|
- /var/run/docker.sock:/var/run/docker.sock
|
||||||
|
networks:
|
||||||
|
- houshan-network
|
||||||
|
|
||||||
|
# ---------------------------------
|
||||||
|
# Database Service
|
||||||
|
# ---------------------------------
|
||||||
|
# postgres:
|
||||||
|
# image: postgres:17-alpine
|
||||||
|
# volumes:
|
||||||
|
# - houshan-postgres:/var/lib/postgresql/data
|
||||||
|
# environment:
|
||||||
|
# POSTGRES_DB: ${API_DB_NAME}
|
||||||
|
# POSTGRES_USER: ${API_DB_USER}
|
||||||
|
# POSTGRES_PASSWORD: ${API_DB_PASS}
|
||||||
|
# ports:
|
||||||
|
# - 54921:5432
|
||||||
|
# networks:
|
||||||
|
# - houshan-network
|
||||||
|
# healthcheck:
|
||||||
|
# test: [ "CMD", "pg_isready", "-q", "-d", "${API_DB_NAME}", "-U", "${API_DB_USER}" ]
|
||||||
|
# interval: 10s
|
||||||
|
# timeout: 5s
|
||||||
|
# retries: 3
|
||||||
|
# start_period: 60s
|
||||||
|
# restart: unless-stopped
|
||||||
|
|
||||||
|
# ---------------------------------
|
||||||
|
# Database Backup Service
|
||||||
|
# ---------------------------------
|
||||||
|
# backups:
|
||||||
|
# image: postgres:17-alpine
|
||||||
|
# command: >-
|
||||||
|
# sh -c 'echo "Starting backups service" && sleep "${API_DB_BACKUP_DELAY}" &&
|
||||||
|
# while true; do
|
||||||
|
# echo "Running pg_dump" && pg_dump -h postgres -p 5432 -d "${API_DB_NAME}" -U "${API_DB_USER}" | gzip > "/srv/houshan-postgres/backups/houshan-postgres-backup-$(date "+%Y-%m-%d_%H-%M").sql" &&
|
||||||
|
# echo "Keeping last 3 backups" && ls -1t "/srv/houshan-postgres/backups/houshan-postgres-backup-"*.gz | tail -n +4 | xargs rm -f &&
|
||||||
|
# echo "Sleeping for ${API_DB_BACKUP_INTERVAL}" && sleep "${API_DB_BACKUP_INTERVAL}"; done'
|
||||||
|
# volumes:
|
||||||
|
# - ./db-backup/houshan-postgres-backup:/var/lib/postgresql/data
|
||||||
|
# - ./db-backup/houshan-database-backups:/srv/houshan-postgres/backups
|
||||||
|
# environment:
|
||||||
|
# PGPASSWORD: ${API_DB_PASS}
|
||||||
|
# networks:
|
||||||
|
# - houshan-network
|
||||||
|
# restart: unless-stopped
|
||||||
|
# depends_on:
|
||||||
|
# postgres:
|
||||||
|
# condition: service_healthy
|
||||||
|
|
||||||
|
# backups:
|
||||||
|
# image: postgres:17-alpine
|
||||||
|
# command: ["/bin/sh", "/usr/local/bin/backup.sh"]
|
||||||
|
# volumes:
|
||||||
|
# - ./backup.sh:/usr/local/bin/backup.sh
|
||||||
|
# - ./db-backup/houshan-database-backups:/srv/houshan-postgres/backups
|
||||||
|
# environment:
|
||||||
|
# PGPASSWORD: ${API_DB_PASS}
|
||||||
|
# API_DB_BACKUP_DELAY: ${API_DB_BACKUP_DELAY}
|
||||||
|
# API_DB_BACKUP_INTERVAL: ${API_DB_BACKUP_INTERVAL}
|
||||||
|
# API_DB_NAME: ${API_DB_NAME}
|
||||||
|
# API_DB_USER: ${API_DB_USER}
|
||||||
|
# networks:
|
||||||
|
# - houshan-network
|
||||||
|
# restart: unless-stopped
|
||||||
|
# depends_on:
|
||||||
|
# postgres:
|
||||||
|
# condition: service_healthy
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------
|
||||||
|
# Frontend Service (Your Flutter App)
|
||||||
|
# ---------------------------------
|
||||||
|
# web:
|
||||||
|
#build:
|
||||||
|
# context: ./frontend
|
||||||
|
# networks:
|
||||||
|
# - houshan-network
|
||||||
|
# restart: unless-stopped
|
||||||
|
|
||||||
|
# ---------------------------------
|
||||||
|
# Main Reverse Proxy (Nginx)
|
||||||
|
# ---------------------------------
|
||||||
|
proxy:
|
||||||
|
image: nginx:alpine
|
||||||
|
ports:
|
||||||
|
- "80:80"
|
||||||
|
volumes:
|
||||||
|
- ./proxy/nginx.conf:/etc/nginx/conf.d/default.conf:ro
|
||||||
|
networks:
|
||||||
|
- houshan-network
|
||||||
|
restart: unless-stopped
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
FROM nginx:alpine
|
||||||
|
|
||||||
|
# Setting the working directory inside the container
|
||||||
|
WORKDIR /usr/share/nginx/html
|
||||||
|
|
||||||
|
# Copying the zip file containing the Flutter web build into the container
|
||||||
|
COPY build.zip /tmp/build.zip
|
||||||
|
|
||||||
|
# Installing unzip, extracting the build files with overwrite, and cleaning up
|
||||||
|
RUN apk add --no-cache unzip && \
|
||||||
|
unzip -o /tmp/build.zip -d /usr/share/nginx/html && \
|
||||||
|
rm /tmp/build.zip && \
|
||||||
|
chown -R nginx:nginx /usr/share/nginx/html
|
||||||
|
|
||||||
|
# Copying a custom Nginx configuration to serve the Flutter app
|
||||||
|
# It looks for 'nginx.conf' in the same directory (the build context)
|
||||||
|
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||||
|
|
||||||
|
# Exposing port 80 (inside the container)
|
||||||
|
EXPOSE 80
|
||||||
|
|
||||||
|
# Starting Nginx in the foreground
|
||||||
|
CMD ["nginx", "-g", "daemon off;"]
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name localhost;
|
||||||
|
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
index index.html;
|
||||||
|
|
||||||
|
# Serving static files from Flutter build
|
||||||
|
location / {
|
||||||
|
try_files $uri $uri/ /index.html; # Fallback to index.html for SPA routing
|
||||||
|
}
|
||||||
|
|
||||||
|
# Caching static assets for better performance
|
||||||
|
location ~* \.(png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot|js|json|wasm)$ {
|
||||||
|
expires 1y;
|
||||||
|
access_log off;
|
||||||
|
add_header Cache-Control "public";
|
||||||
|
}
|
||||||
|
|
||||||
|
# Handling CORS for Flutter web
|
||||||
|
location ~* \.(js|json|wasm)$ {
|
||||||
|
add_header 'Access-Control-Allow-Origin' '*';
|
||||||
|
add_header 'Access-Control-Allow-Methods' 'GET, HEAD';
|
||||||
|
}
|
||||||
|
|
||||||
|
# Logging configuration
|
||||||
|
access_log /var/log/nginx/access.log;
|
||||||
|
error_log /var/log/nginx/error.log;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,173 @@
|
||||||
|
from tortoise import BaseDBAsyncClient
|
||||||
|
|
||||||
|
|
||||||
|
async def upgrade(db: BaseDBAsyncClient) -> str:
|
||||||
|
return """
|
||||||
|
CREATE TABLE IF NOT EXISTS "categories" (
|
||||||
|
"id" SERIAL NOT NULL PRIMARY KEY,
|
||||||
|
"name" VARCHAR(255) NOT NULL
|
||||||
|
);
|
||||||
|
CREATE TABLE IF NOT EXISTS "discounts" (
|
||||||
|
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updated_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"id" SERIAL NOT NULL PRIMARY KEY,
|
||||||
|
"code" VARCHAR(8) NOT NULL,
|
||||||
|
"percent" INT,
|
||||||
|
"value" INT,
|
||||||
|
"max_value" INT,
|
||||||
|
"expires_at" TIMESTAMPTZ
|
||||||
|
);
|
||||||
|
CREATE TABLE IF NOT EXISTS "users" (
|
||||||
|
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updated_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"id" UUID NOT NULL PRIMARY KEY,
|
||||||
|
"mobile_number" VARCHAR(11) UNIQUE,
|
||||||
|
"password" VARCHAR(255),
|
||||||
|
"name" VARCHAR(255),
|
||||||
|
"image" VARCHAR(255),
|
||||||
|
"email" VARCHAR(255) UNIQUE,
|
||||||
|
"username" VARCHAR(255) UNIQUE,
|
||||||
|
"verified" BOOL NOT NULL DEFAULT False,
|
||||||
|
"otp" VARCHAR(6),
|
||||||
|
"otp_expires_at" TIMESTAMPTZ,
|
||||||
|
"code" VARCHAR(6),
|
||||||
|
"card_number" VARCHAR(16),
|
||||||
|
"income" DOUBLE PRECISION NOT NULL DEFAULT 0,
|
||||||
|
"credit" INT NOT NULL DEFAULT 0,
|
||||||
|
"free_credit" INT NOT NULL DEFAULT 100
|
||||||
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS "idx_users_mobile__c5f07a" ON "users" ("mobile_number");
|
||||||
|
CREATE INDEX IF NOT EXISTS "idx_users_email_133a6f" ON "users" ("email");
|
||||||
|
CREATE INDEX IF NOT EXISTS "idx_users_usernam_266d85" ON "users" ("username");
|
||||||
|
CREATE TABLE IF NOT EXISTS "billings" (
|
||||||
|
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updated_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"id" SERIAL NOT NULL PRIMARY KEY,
|
||||||
|
"amount" INT NOT NULL,
|
||||||
|
"credit" INT NOT NULL,
|
||||||
|
"discount" INT NOT NULL DEFAULT 0,
|
||||||
|
"authority" VARCHAR(255) UNIQUE,
|
||||||
|
"ref_id" VARCHAR(255) UNIQUE,
|
||||||
|
"type" VARCHAR(9) NOT NULL DEFAULT 'purchase',
|
||||||
|
"status" VARCHAR(7) NOT NULL DEFAULT 'pending',
|
||||||
|
"user_id" UUID REFERENCES "users" ("id") ON DELETE SET NULL
|
||||||
|
);
|
||||||
|
COMMENT ON COLUMN "billings"."type" IS 'FREE: free\nCODE: code\nINVITE: invite\nWELCOME: welcome\nPURCHASE: purchase\nINSTAGRAM: instagram';
|
||||||
|
COMMENT ON COLUMN "billings"."status" IS 'SUCCESS: success\nFAILED: failed\nPENDING: pending';
|
||||||
|
CREATE TABLE IF NOT EXISTS "bots" (
|
||||||
|
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updated_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"id" SERIAL NOT NULL PRIMARY KEY,
|
||||||
|
"name" VARCHAR(255),
|
||||||
|
"model" VARCHAR(255) NOT NULL,
|
||||||
|
"description" TEXT,
|
||||||
|
"image" VARCHAR(255) NOT NULL,
|
||||||
|
"attachment" INT NOT NULL DEFAULT 0,
|
||||||
|
"attachment_type" JSONB,
|
||||||
|
"tool" BOOL NOT NULL DEFAULT False,
|
||||||
|
"public" BOOL NOT NULL DEFAULT True,
|
||||||
|
"cost" INT NOT NULL DEFAULT 0,
|
||||||
|
"limit" INT NOT NULL DEFAULT -1,
|
||||||
|
"prompt" TEXT,
|
||||||
|
"links" JSONB,
|
||||||
|
"docs" JSONB,
|
||||||
|
"status" VARCHAR(9) NOT NULL DEFAULT 'confirmed',
|
||||||
|
"category_id" INT REFERENCES "categories" ("id") ON DELETE SET NULL,
|
||||||
|
"user_id" UUID REFERENCES "users" ("id") ON DELETE SET NULL
|
||||||
|
);
|
||||||
|
COMMENT ON COLUMN "bots"."status" IS 'CONFIRMED: confirmed\nREJECTED: rejected\nPENDING: pending';
|
||||||
|
CREATE TABLE IF NOT EXISTS "chatbots" (
|
||||||
|
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updated_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"id" SERIAL NOT NULL PRIMARY KEY,
|
||||||
|
"title" VARCHAR(255),
|
||||||
|
"archive" BOOL NOT NULL DEFAULT False,
|
||||||
|
"bot_id" INT NOT NULL REFERENCES "bots" ("id") ON DELETE CASCADE,
|
||||||
|
"user_id" UUID NOT NULL REFERENCES "users" ("id") ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
CREATE TABLE IF NOT EXISTS "comments" (
|
||||||
|
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updated_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"id" SERIAL NOT NULL PRIMARY KEY,
|
||||||
|
"text" TEXT NOT NULL,
|
||||||
|
"image" VARCHAR(255),
|
||||||
|
"category_id" INT NOT NULL REFERENCES "categories" ("id") ON DELETE CASCADE,
|
||||||
|
"parent_id" INT REFERENCES "comments" ("id") ON DELETE SET NULL,
|
||||||
|
"replied_user_id" UUID REFERENCES "users" ("id") ON DELETE SET NULL,
|
||||||
|
"user_id" UUID NOT NULL REFERENCES "users" ("id") ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
CREATE TABLE IF NOT EXISTS "feedbacks" (
|
||||||
|
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updated_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"id" SERIAL NOT NULL PRIMARY KEY,
|
||||||
|
"status" VARCHAR(8) NOT NULL,
|
||||||
|
"comment_id" INT NOT NULL REFERENCES "comments" ("id") ON DELETE CASCADE,
|
||||||
|
"user_id" UUID NOT NULL REFERENCES "users" ("id") ON DELETE CASCADE,
|
||||||
|
CONSTRAINT "uid_feedbacks_user_id_c72030" UNIQUE ("user_id", "comment_id")
|
||||||
|
);
|
||||||
|
COMMENT ON COLUMN "feedbacks"."status" IS 'LIKED: liked\nDISLIKED: disliked';
|
||||||
|
CREATE TABLE IF NOT EXISTS "marks" (
|
||||||
|
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updated_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"id" SERIAL NOT NULL PRIMARY KEY,
|
||||||
|
"bot_id" INT NOT NULL REFERENCES "bots" ("id") ON DELETE CASCADE,
|
||||||
|
"user_id" UUID NOT NULL REFERENCES "users" ("id") ON DELETE CASCADE,
|
||||||
|
CONSTRAINT "uid_marks_user_id_76ce59" UNIQUE ("user_id", "bot_id")
|
||||||
|
);
|
||||||
|
CREATE TABLE IF NOT EXISTS "messages" (
|
||||||
|
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updated_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"id" SERIAL NOT NULL PRIMARY KEY,
|
||||||
|
"input_tokens" INT NOT NULL,
|
||||||
|
"output_tokens" INT NOT NULL,
|
||||||
|
"chatbot_id" INT REFERENCES "chatbots" ("id") ON DELETE SET NULL
|
||||||
|
);
|
||||||
|
CREATE TABLE IF NOT EXISTS "notifications" (
|
||||||
|
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updated_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"id" SERIAL NOT NULL PRIMARY KEY,
|
||||||
|
"title" VARCHAR(255) NOT NULL,
|
||||||
|
"message" VARCHAR(255) NOT NULL,
|
||||||
|
"seen" BOOL NOT NULL DEFAULT False,
|
||||||
|
"user_id" UUID NOT NULL REFERENCES "users" ("id") ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
CREATE TABLE IF NOT EXISTS "rates" (
|
||||||
|
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updated_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"id" SERIAL NOT NULL PRIMARY KEY,
|
||||||
|
"text" TEXT NOT NULL,
|
||||||
|
"score" INT NOT NULL,
|
||||||
|
"bot_id" INT NOT NULL REFERENCES "bots" ("id") ON DELETE CASCADE,
|
||||||
|
"user_id" UUID NOT NULL REFERENCES "users" ("id") ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
CREATE TABLE IF NOT EXISTS "settlements" (
|
||||||
|
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updated_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"id" SERIAL NOT NULL PRIMARY KEY,
|
||||||
|
"income" INT NOT NULL,
|
||||||
|
"confirmed" BOOL NOT NULL DEFAULT False,
|
||||||
|
"type" VARCHAR(5) NOT NULL,
|
||||||
|
"user_id" UUID NOT NULL REFERENCES "users" ("id") ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
COMMENT ON COLUMN "settlements"."type" IS 'COIN: coin\nMONEY: money';
|
||||||
|
CREATE TABLE IF NOT EXISTS "tickets" (
|
||||||
|
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updated_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"id" SERIAL NOT NULL PRIMARY KEY,
|
||||||
|
"text" TEXT,
|
||||||
|
"role" VARCHAR(5) NOT NULL DEFAULT 'user',
|
||||||
|
"file" VARCHAR(255),
|
||||||
|
"user_id" UUID NOT NULL REFERENCES "users" ("id") ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
COMMENT ON COLUMN "tickets"."role" IS 'USER: user\nADMIN: admin';
|
||||||
|
CREATE TABLE IF NOT EXISTS "aerich" (
|
||||||
|
"id" SERIAL NOT NULL PRIMARY KEY,
|
||||||
|
"version" VARCHAR(255) NOT NULL,
|
||||||
|
"app" VARCHAR(100) NOT NULL,
|
||||||
|
"content" JSONB NOT NULL
|
||||||
|
);"""
|
||||||
|
|
||||||
|
|
||||||
|
async def downgrade(db: BaseDBAsyncClient) -> str:
|
||||||
|
return """
|
||||||
|
"""
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
from tortoise import BaseDBAsyncClient
|
||||||
|
|
||||||
|
|
||||||
|
async def upgrade(db: BaseDBAsyncClient) -> str:
|
||||||
|
return """
|
||||||
|
ALTER TABLE "categories" ADD "image" VARCHAR(255);
|
||||||
|
ALTER TABLE "categories" ADD "description" TEXT;
|
||||||
|
ALTER TABLE "categories" ADD "tool" BOOL NOT NULL DEFAULT False;"""
|
||||||
|
|
||||||
|
|
||||||
|
async def downgrade(db: BaseDBAsyncClient) -> str:
|
||||||
|
return """
|
||||||
|
ALTER TABLE "categories" DROP COLUMN "image";
|
||||||
|
ALTER TABLE "categories" DROP COLUMN "description";
|
||||||
|
ALTER TABLE "categories" DROP COLUMN "tool";"""
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
from tortoise import BaseDBAsyncClient
|
||||||
|
|
||||||
|
|
||||||
|
async def upgrade(db: BaseDBAsyncClient) -> str:
|
||||||
|
return """
|
||||||
|
ALTER TABLE "banners" ADD "bot_id" INT;
|
||||||
|
ALTER TABLE "banners" ADD CONSTRAINT "fk_banners_bots_1c5a6f42" FOREIGN KEY ("bot_id") REFERENCES "bots" ("id") ON DELETE SET NULL;"""
|
||||||
|
|
||||||
|
|
||||||
|
async def downgrade(db: BaseDBAsyncClient) -> str:
|
||||||
|
return """
|
||||||
|
ALTER TABLE "banners" DROP CONSTRAINT "fk_banners_bots_1c5a6f42";
|
||||||
|
ALTER TABLE "banners" DROP COLUMN "bot_id";"""
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
from tortoise import BaseDBAsyncClient
|
||||||
|
|
||||||
|
|
||||||
|
async def upgrade(db: BaseDBAsyncClient) -> str:
|
||||||
|
return """
|
||||||
|
ALTER TABLE "banners" ADD "link" VARCHAR(255);
|
||||||
|
ALTER TABLE "banners" ALTER COLUMN "title" SET DEFAULT '';
|
||||||
|
ALTER TABLE "banners" ALTER COLUMN "title" SET NOT NULL;
|
||||||
|
ALTER TABLE "banners" ALTER COLUMN "description" SET DEFAULT '';
|
||||||
|
ALTER TABLE "banners" ALTER COLUMN "description" SET NOT NULL;"""
|
||||||
|
|
||||||
|
|
||||||
|
async def downgrade(db: BaseDBAsyncClient) -> str:
|
||||||
|
return """
|
||||||
|
ALTER TABLE "banners" DROP COLUMN "link";
|
||||||
|
ALTER TABLE "banners" ALTER COLUMN "title" DROP NOT NULL;
|
||||||
|
ALTER TABLE "banners" ALTER COLUMN "title" DROP DEFAULT;
|
||||||
|
ALTER TABLE "banners" ALTER COLUMN "description" DROP NOT NULL;
|
||||||
|
ALTER TABLE "banners" ALTER COLUMN "description" DROP DEFAULT;"""
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
from tortoise import BaseDBAsyncClient
|
||||||
|
|
||||||
|
|
||||||
|
async def upgrade(db: BaseDBAsyncClient) -> str:
|
||||||
|
return """
|
||||||
|
ALTER TABLE "bots" ADD "deleted" BOOL NOT NULL DEFAULT False;"""
|
||||||
|
|
||||||
|
|
||||||
|
async def downgrade(db: BaseDBAsyncClient) -> str:
|
||||||
|
return """
|
||||||
|
ALTER TABLE "bots" DROP COLUMN "deleted";"""
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
from tortoise import BaseDBAsyncClient
|
||||||
|
|
||||||
|
|
||||||
|
async def upgrade(db: BaseDBAsyncClient) -> str:
|
||||||
|
return """
|
||||||
|
CREATE TABLE IF NOT EXISTS "code" (
|
||||||
|
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updated_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"id" SERIAL NOT NULL PRIMARY KEY,
|
||||||
|
"usage_count" INT NOT NULL DEFAULT 0,
|
||||||
|
"code" VARCHAR(6) NOT NULL UNIQUE,
|
||||||
|
"message" VARCHAR(255)
|
||||||
|
);
|
||||||
|
ALTER TABLE "plans" ADD "description" VARCHAR(255);"""
|
||||||
|
|
||||||
|
|
||||||
|
async def downgrade(db: BaseDBAsyncClient) -> str:
|
||||||
|
return """
|
||||||
|
ALTER TABLE "plans" DROP COLUMN "description";
|
||||||
|
DROP TABLE IF EXISTS "code";"""
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
from tortoise import BaseDBAsyncClient
|
||||||
|
|
||||||
|
|
||||||
|
async def upgrade(db: BaseDBAsyncClient) -> str:
|
||||||
|
return """
|
||||||
|
ALTER TABLE "bots" ADD "order" INT NOT NULL DEFAULT 1;"""
|
||||||
|
|
||||||
|
|
||||||
|
async def downgrade(db: BaseDBAsyncClient) -> str:
|
||||||
|
return """
|
||||||
|
ALTER TABLE "bots" DROP COLUMN "order";"""
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
from tortoise import BaseDBAsyncClient
|
||||||
|
|
||||||
|
|
||||||
|
async def upgrade(db: BaseDBAsyncClient) -> str:
|
||||||
|
return """
|
||||||
|
ALTER TABLE "notifications" ALTER COLUMN "message" TYPE TEXT USING "message"::TEXT;"""
|
||||||
|
|
||||||
|
|
||||||
|
async def downgrade(db: BaseDBAsyncClient) -> str:
|
||||||
|
return """
|
||||||
|
ALTER TABLE "notifications" ALTER COLUMN "message" TYPE VARCHAR(255) USING "message"::VARCHAR(255);"""
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
from tortoise import BaseDBAsyncClient
|
||||||
|
|
||||||
|
|
||||||
|
async def upgrade(db: BaseDBAsyncClient) -> str:
|
||||||
|
return """
|
||||||
|
ALTER TABLE "messages" ADD "cost" INT NOT NULL DEFAULT -1;
|
||||||
|
ALTER TABLE "users" ALTER COLUMN "free_credit" SET DEFAULT 50;"""
|
||||||
|
|
||||||
|
|
||||||
|
async def downgrade(db: BaseDBAsyncClient) -> str:
|
||||||
|
return """
|
||||||
|
ALTER TABLE "users" ALTER COLUMN "free_credit" SET DEFAULT 100;
|
||||||
|
ALTER TABLE "messages" DROP COLUMN "cost";"""
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
from tortoise import BaseDBAsyncClient
|
||||||
|
|
||||||
|
|
||||||
|
async def upgrade(db: BaseDBAsyncClient) -> str:
|
||||||
|
return """
|
||||||
|
ALTER TABLE "bots" ADD "web_search" BOOL NOT NULL DEFAULT False;"""
|
||||||
|
|
||||||
|
|
||||||
|
async def downgrade(db: BaseDBAsyncClient) -> str:
|
||||||
|
return """
|
||||||
|
ALTER TABLE "bots" DROP COLUMN "web_search";"""
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
from tortoise import BaseDBAsyncClient
|
||||||
|
|
||||||
|
|
||||||
|
async def upgrade(db: BaseDBAsyncClient) -> str:
|
||||||
|
return """
|
||||||
|
ALTER TABLE "categories" ADD "character" BOOL NOT NULL DEFAULT False;"""
|
||||||
|
|
||||||
|
|
||||||
|
async def downgrade(db: BaseDBAsyncClient) -> str:
|
||||||
|
return """
|
||||||
|
ALTER TABLE "categories" DROP COLUMN "character";"""
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
from tortoise import BaseDBAsyncClient
|
||||||
|
|
||||||
|
|
||||||
|
async def upgrade(db: BaseDBAsyncClient) -> str:
|
||||||
|
return """
|
||||||
|
ALTER TABLE "billings" ADD "code" VARCHAR(8);"""
|
||||||
|
|
||||||
|
|
||||||
|
async def downgrade(db: BaseDBAsyncClient) -> str:
|
||||||
|
return """
|
||||||
|
ALTER TABLE "billings" DROP COLUMN "code";"""
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
from tortoise import BaseDBAsyncClient
|
||||||
|
|
||||||
|
|
||||||
|
async def upgrade(db: BaseDBAsyncClient) -> str:
|
||||||
|
return """
|
||||||
|
ALTER TABLE "categories" ADD "media" BOOL NOT NULL DEFAULT False;"""
|
||||||
|
|
||||||
|
|
||||||
|
async def downgrade(db: BaseDBAsyncClient) -> str:
|
||||||
|
return """
|
||||||
|
ALTER TABLE "categories" DROP COLUMN "media";"""
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
from tortoise import BaseDBAsyncClient
|
||||||
|
|
||||||
|
|
||||||
|
async def upgrade(db: BaseDBAsyncClient) -> str:
|
||||||
|
return """
|
||||||
|
ALTER TABLE "categories" ADD "parent_category_id" INT;
|
||||||
|
ALTER TABLE "categories" ADD CONSTRAINT "fk_categori_categori_6c119c37" FOREIGN KEY ("parent_category_id") REFERENCES "categories" ("id") ON DELETE SET NULL;"""
|
||||||
|
|
||||||
|
|
||||||
|
async def downgrade(db: BaseDBAsyncClient) -> str:
|
||||||
|
return """
|
||||||
|
ALTER TABLE "categories" DROP CONSTRAINT "fk_categori_categori_6c119c37";
|
||||||
|
ALTER TABLE "categories" DROP COLUMN "parent_category_id";"""
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
from tortoise import BaseDBAsyncClient
|
||||||
|
|
||||||
|
|
||||||
|
async def upgrade(db: BaseDBAsyncClient) -> str:
|
||||||
|
return """
|
||||||
|
ALTER TABLE "bots" ADD "style" VARCHAR(255);"""
|
||||||
|
|
||||||
|
|
||||||
|
async def downgrade(db: BaseDBAsyncClient) -> str:
|
||||||
|
return """
|
||||||
|
ALTER TABLE "bots" DROP COLUMN "style";"""
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
from tortoise import BaseDBAsyncClient
|
||||||
|
|
||||||
|
|
||||||
|
async def upgrade(db: BaseDBAsyncClient) -> str:
|
||||||
|
return """
|
||||||
|
CREATE TABLE IF NOT EXISTS "suno_tasks" (
|
||||||
|
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updated_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"id" UUID NOT NULL PRIMARY KEY,
|
||||||
|
"task_id" VARCHAR(255) NOT NULL UNIQUE,
|
||||||
|
"status" VARCHAR(50) NOT NULL DEFAULT 'PENDING',
|
||||||
|
"audio_url" VARCHAR(500),
|
||||||
|
"image_url" VARCHAR(500),
|
||||||
|
"audio_url2" VARCHAR(500),
|
||||||
|
"image_url2" VARCHAR(500),
|
||||||
|
"chatbot_id" INT NOT NULL REFERENCES "chatbots" ("id") ON DELETE CASCADE,
|
||||||
|
"user_id" UUID NOT NULL REFERENCES "users" ("id") ON DELETE CASCADE
|
||||||
|
);"""
|
||||||
|
|
||||||
|
|
||||||
|
async def downgrade(db: BaseDBAsyncClient) -> str:
|
||||||
|
return """
|
||||||
|
DROP TABLE IF EXISTS "suno_tasks";"""
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
from tortoise import BaseDBAsyncClient
|
||||||
|
|
||||||
|
|
||||||
|
async def upgrade(db: BaseDBAsyncClient) -> str:
|
||||||
|
return """
|
||||||
|
ALTER TABLE "bots" ADD "type" VARCHAR(9) NOT NULL DEFAULT 'llm';"""
|
||||||
|
|
||||||
|
|
||||||
|
async def downgrade(db: BaseDBAsyncClient) -> str:
|
||||||
|
return """
|
||||||
|
ALTER TABLE "bots" DROP COLUMN "type";"""
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
from tortoise import BaseDBAsyncClient
|
||||||
|
|
||||||
|
|
||||||
|
async def upgrade(db: BaseDBAsyncClient) -> str:
|
||||||
|
return """
|
||||||
|
CREATE TABLE IF NOT EXISTS "events" (
|
||||||
|
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updated_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"id" SERIAL NOT NULL PRIMARY KEY,
|
||||||
|
"title" VARCHAR(255) NOT NULL,
|
||||||
|
"description" TEXT NOT NULL,
|
||||||
|
"awards" TEXT NOT NULL,
|
||||||
|
"image" VARCHAR(255),
|
||||||
|
"start_at" TIMESTAMPTZ NOT NULL,
|
||||||
|
"end_at" TIMESTAMPTZ NOT NULL,
|
||||||
|
"is_open" BOOL NOT NULL DEFAULT True,
|
||||||
|
"total_participants" INT NOT NULL DEFAULT 0,
|
||||||
|
"total_received_works" INT NOT NULL DEFAULT 0
|
||||||
|
);
|
||||||
|
CREATE TABLE IF NOT EXISTS "winners" (
|
||||||
|
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updated_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"id" SERIAL NOT NULL PRIMARY KEY,
|
||||||
|
"rank" INT NOT NULL,
|
||||||
|
"event_id" INT NOT NULL REFERENCES "events" ("id") ON DELETE CASCADE,
|
||||||
|
"user_id" UUID NOT NULL REFERENCES "users" ("id") ON DELETE CASCADE
|
||||||
|
);"""
|
||||||
|
|
||||||
|
|
||||||
|
async def downgrade(db: BaseDBAsyncClient) -> str:
|
||||||
|
return """
|
||||||
|
DROP TABLE IF EXISTS "events";
|
||||||
|
DROP TABLE IF EXISTS "winners";"""
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
from tortoise import BaseDBAsyncClient
|
||||||
|
|
||||||
|
|
||||||
|
async def upgrade(db: BaseDBAsyncClient) -> str:
|
||||||
|
return """
|
||||||
|
ALTER TABLE "bots" ADD "image_2" VARCHAR(255);"""
|
||||||
|
|
||||||
|
|
||||||
|
async def downgrade(db: BaseDBAsyncClient) -> str:
|
||||||
|
return """
|
||||||
|
ALTER TABLE "bots" DROP COLUMN "image_2";"""
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
from tortoise import BaseDBAsyncClient
|
||||||
|
|
||||||
|
|
||||||
|
async def upgrade(db: BaseDBAsyncClient) -> str:
|
||||||
|
return """
|
||||||
|
ALTER TABLE "categories" ADD "icon" VARCHAR(255);"""
|
||||||
|
|
||||||
|
|
||||||
|
async def downgrade(db: BaseDBAsyncClient) -> str:
|
||||||
|
return """
|
||||||
|
ALTER TABLE "categories" DROP COLUMN "icon";"""
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
from tortoise import BaseDBAsyncClient
|
||||||
|
|
||||||
|
|
||||||
|
async def upgrade(db: BaseDBAsyncClient) -> str:
|
||||||
|
return """
|
||||||
|
ALTER TABLE "events" ADD "subtitle" VARCHAR(255) NOT NULL DEFAULT '';"""
|
||||||
|
|
||||||
|
|
||||||
|
async def downgrade(db: BaseDBAsyncClient) -> str:
|
||||||
|
return """
|
||||||
|
ALTER TABLE "events" DROP COLUMN "subtitle";"""
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
from tortoise import BaseDBAsyncClient
|
||||||
|
|
||||||
|
|
||||||
|
async def upgrade(db: BaseDBAsyncClient) -> str:
|
||||||
|
return """
|
||||||
|
ALTER TABLE "users" ADD "telegram_id" VARCHAR(20);"""
|
||||||
|
|
||||||
|
|
||||||
|
async def downgrade(db: BaseDBAsyncClient) -> str:
|
||||||
|
return """
|
||||||
|
ALTER TABLE "users" DROP COLUMN "telegram_id";"""
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
from tortoise import BaseDBAsyncClient
|
||||||
|
|
||||||
|
|
||||||
|
async def upgrade(db: BaseDBAsyncClient) -> str:
|
||||||
|
return """
|
||||||
|
ALTER TABLE "rates" ALTER COLUMN "score" TYPE DOUBLE PRECISION USING "score"::DOUBLE PRECISION;"""
|
||||||
|
|
||||||
|
|
||||||
|
async def downgrade(db: BaseDBAsyncClient) -> str:
|
||||||
|
return """
|
||||||
|
ALTER TABLE "rates" ALTER COLUMN "score" TYPE INT USING "score"::INT;"""
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
from tortoise import BaseDBAsyncClient
|
||||||
|
|
||||||
|
|
||||||
|
async def upgrade(db: BaseDBAsyncClient) -> str:
|
||||||
|
return """
|
||||||
|
CREATE TABLE IF NOT EXISTS "app_metadata" (
|
||||||
|
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updated_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"tag" VARCHAR(24) NOT NULL PRIMARY KEY,
|
||||||
|
"value" TEXT
|
||||||
|
);"""
|
||||||
|
|
||||||
|
|
||||||
|
async def downgrade(db: BaseDBAsyncClient) -> str:
|
||||||
|
return """
|
||||||
|
DROP TABLE IF EXISTS "app_metadata";"""
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
from tortoise import BaseDBAsyncClient
|
||||||
|
|
||||||
|
|
||||||
|
async def upgrade(db: BaseDBAsyncClient) -> str:
|
||||||
|
return """
|
||||||
|
CREATE TABLE IF NOT EXISTS "advertisements" (
|
||||||
|
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updated_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"id" SERIAL NOT NULL PRIMARY KEY,
|
||||||
|
"ad_type" VARCHAR(255) NOT NULL,
|
||||||
|
"timestamp" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"user_id" UUID NOT NULL REFERENCES "users" ("id") ON DELETE CASCADE
|
||||||
|
);"""
|
||||||
|
|
||||||
|
|
||||||
|
async def downgrade(db: BaseDBAsyncClient) -> str:
|
||||||
|
return """
|
||||||
|
DROP TABLE IF EXISTS "advertisements";"""
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
from tortoise import BaseDBAsyncClient
|
||||||
|
|
||||||
|
async def upgrade(db: BaseDBAsyncClient) -> str:
|
||||||
|
return """
|
||||||
|
ALTER TABLE "users" ADD "level" INT NOT NULL DEFAULT 1;"""
|
||||||
|
|
||||||
|
async def downgrade(db: BaseDBAsyncClient) -> str:
|
||||||
|
return """
|
||||||
|
ALTER TABLE "users" DROP COLUMN "level";"""
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
from tortoise import BaseDBAsyncClient
|
||||||
|
|
||||||
|
async def upgrade(db: BaseDBAsyncClient) -> str:
|
||||||
|
return """
|
||||||
|
ALTER TABLE "bots" ADD "allowed_levels" JSONB;"""
|
||||||
|
|
||||||
|
async def downgrade(db: BaseDBAsyncClient) -> str:
|
||||||
|
return """
|
||||||
|
ALTER TABLE "bots" DROP COLUMN "allowed_levels";"""
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
from tortoise import BaseDBAsyncClient
|
||||||
|
|
||||||
|
async def upgrade(db: BaseDBAsyncClient) -> str:
|
||||||
|
return """
|
||||||
|
ALTER TABLE "users" ADD "parent_id" UUID;
|
||||||
|
ALTER TABLE "users" ADD CONSTRAINT "fk_users_users_parent" FOREIGN KEY ("parent_id") REFERENCES "users" ("id") ON DELETE SET NULL;"""
|
||||||
|
|
||||||
|
async def downgrade(db: BaseDBAsyncClient) -> str:
|
||||||
|
return """
|
||||||
|
ALTER TABLE "users" DROP CONSTRAINT "fk_users_users_parent";
|
||||||
|
ALTER TABLE "users" DROP COLUMN "parent_id";"""
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
from tortoise import BaseDBAsyncClient
|
||||||
|
|
||||||
|
|
||||||
|
async def upgrade(db: BaseDBAsyncClient) -> str:
|
||||||
|
return """
|
||||||
|
ALTER TABLE "bots" ALTER COLUMN "image" DROP NOT NULL;"""
|
||||||
|
|
||||||
|
|
||||||
|
async def downgrade(db: BaseDBAsyncClient) -> str:
|
||||||
|
return """
|
||||||
|
ALTER TABLE "bots" ALTER COLUMN "image" SET NOT NULL;"""
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
from tortoise import BaseDBAsyncClient
|
||||||
|
|
||||||
|
|
||||||
|
async def upgrade(db: BaseDBAsyncClient) -> str:
|
||||||
|
return """
|
||||||
|
CREATE TABLE IF NOT EXISTS "banners" (
|
||||||
|
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updated_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"id" SERIAL NOT NULL PRIMARY KEY,
|
||||||
|
"image" VARCHAR(255) NOT NULL,
|
||||||
|
"description" TEXT NOT NULL
|
||||||
|
);"""
|
||||||
|
|
||||||
|
|
||||||
|
async def downgrade(db: BaseDBAsyncClient) -> str:
|
||||||
|
return """
|
||||||
|
DROP TABLE IF EXISTS "banners";"""
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
from tortoise import BaseDBAsyncClient
|
||||||
|
|
||||||
|
|
||||||
|
async def upgrade(db: BaseDBAsyncClient) -> str:
|
||||||
|
return """
|
||||||
|
ALTER TABLE "banners" ADD "title" VARCHAR(255);
|
||||||
|
ALTER TABLE "banners" ALTER COLUMN "description" DROP DEFAULT;
|
||||||
|
ALTER TABLE "banners" ALTER COLUMN "description" DROP NOT NULL;
|
||||||
|
ALTER TABLE "banners" ALTER COLUMN "description" TYPE VARCHAR(255) USING "description"::VARCHAR(255);"""
|
||||||
|
|
||||||
|
|
||||||
|
async def downgrade(db: BaseDBAsyncClient) -> str:
|
||||||
|
return """
|
||||||
|
ALTER TABLE "banners" DROP COLUMN "title";
|
||||||
|
ALTER TABLE "banners" ALTER COLUMN "description" TYPE TEXT USING "description"::TEXT;
|
||||||
|
ALTER TABLE "banners" ALTER COLUMN "description" SET NOT NULL;
|
||||||
|
ALTER TABLE "banners" ALTER COLUMN "description" SET;"""
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
from tortoise import BaseDBAsyncClient
|
||||||
|
|
||||||
|
|
||||||
|
async def upgrade(db: BaseDBAsyncClient) -> str:
|
||||||
|
return """
|
||||||
|
ALTER TABLE "users" ADD "role" VARCHAR(5) NOT NULL DEFAULT 'user';"""
|
||||||
|
|
||||||
|
|
||||||
|
async def downgrade(db: BaseDBAsyncClient) -> str:
|
||||||
|
return """
|
||||||
|
ALTER TABLE "users" DROP COLUMN "role";"""
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
from tortoise import BaseDBAsyncClient
|
||||||
|
|
||||||
|
|
||||||
|
async def upgrade(db: BaseDBAsyncClient) -> str:
|
||||||
|
return """
|
||||||
|
CREATE TABLE IF NOT EXISTS "plans" (
|
||||||
|
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updated_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"id" VARCHAR(20) NOT NULL PRIMARY KEY,
|
||||||
|
"price" INT NOT NULL,
|
||||||
|
"coins" INT NOT NULL,
|
||||||
|
"free_coins" INT NOT NULL,
|
||||||
|
"title" VARCHAR(255) NOT NULL,
|
||||||
|
"active" BOOL NOT NULL DEFAULT False
|
||||||
|
);"""
|
||||||
|
|
||||||
|
|
||||||
|
async def downgrade(db: BaseDBAsyncClient) -> str:
|
||||||
|
return """
|
||||||
|
DROP TABLE IF EXISTS "plans";"""
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
from tortoise import BaseDBAsyncClient
|
||||||
|
|
||||||
|
|
||||||
|
async def upgrade(db: BaseDBAsyncClient) -> str:
|
||||||
|
return """
|
||||||
|
ALTER TABLE "users" ADD "firebase_token" VARCHAR(255);"""
|
||||||
|
|
||||||
|
|
||||||
|
async def downgrade(db: BaseDBAsyncClient) -> str:
|
||||||
|
return """
|
||||||
|
ALTER TABLE "users" DROP COLUMN "firebase_token";"""
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
from tortoise import BaseDBAsyncClient
|
||||||
|
|
||||||
|
|
||||||
|
async def upgrade(db: BaseDBAsyncClient) -> str:
|
||||||
|
return """
|
||||||
|
ALTER TABLE "plans" ADD "image" VARCHAR(255);"""
|
||||||
|
|
||||||
|
|
||||||
|
async def downgrade(db: BaseDBAsyncClient) -> str:
|
||||||
|
return """
|
||||||
|
ALTER TABLE "plans" DROP COLUMN "image";"""
|
||||||
|
|
@ -0,0 +1,93 @@
|
||||||
|
# # Upstream for the API
|
||||||
|
upstream api_server {
|
||||||
|
server basa_api:8000;
|
||||||
|
}
|
||||||
|
|
||||||
|
# # Upstream for the Frontend
|
||||||
|
# upstream panel_server {
|
||||||
|
# server nextjs_app:3000;
|
||||||
|
# }
|
||||||
|
|
||||||
|
# upstream plan_server {
|
||||||
|
# server basa_plans:6000;
|
||||||
|
# }
|
||||||
|
|
||||||
|
|
||||||
|
# Server block for your API
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name basa.houshan.ai;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
proxy_pass http://api_server;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
# server {
|
||||||
|
# listen 80;
|
||||||
|
# server_name basapanel.houshan.ai;
|
||||||
|
|
||||||
|
# location / {
|
||||||
|
# proxy_pass http://panel_server;
|
||||||
|
|
||||||
|
# proxy_set_header Host $host;
|
||||||
|
# proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
# proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
# proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
|
||||||
|
# proxy_http_version 1.1;
|
||||||
|
# proxy_set_header Upgrade $http_upgrade;
|
||||||
|
# proxy_set_header Connection "upgrade";
|
||||||
|
# }
|
||||||
|
# }
|
||||||
|
|
||||||
|
# server {
|
||||||
|
# listen 80;
|
||||||
|
# server_name basaplans.houshan.ai;
|
||||||
|
|
||||||
|
# location / {
|
||||||
|
# proxy_pass http://plan_server;
|
||||||
|
|
||||||
|
# proxy_set_header Host $host;
|
||||||
|
# proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
# proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
# proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
|
||||||
|
# proxy_http_version 1.1;
|
||||||
|
# proxy_set_header Upgrade $http_upgrade;
|
||||||
|
# proxy_set_header Connection "upgrade";
|
||||||
|
# }
|
||||||
|
# }
|
||||||
|
# --- FLUTTER FRONTEND ---
|
||||||
|
# server {
|
||||||
|
# listen 80;
|
||||||
|
# server_name basaweb.houshan.ai;
|
||||||
|
|
||||||
|
# location / {
|
||||||
|
# resolver 127.0.0.11 valid=30s;
|
||||||
|
# set $upstream_flutter basa_flutter:80;
|
||||||
|
# proxy_pass http://$upstream_flutter;
|
||||||
|
# proxy_set_header Host $host;
|
||||||
|
# proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
# proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
# proxy_set_header X-Forwarded-Proto http;
|
||||||
|
|
||||||
|
# # Support for WebSockets
|
||||||
|
# proxy_http_version 1.1;
|
||||||
|
# proxy_set_header Upgrade $http_upgrade;
|
||||||
|
# proxy_set_header Connection "upgrade";
|
||||||
|
# }
|
||||||
|
# }
|
||||||
|
# A default server to catch any other requests
|
||||||
|
server {
|
||||||
|
listen 80 default_server;
|
||||||
|
server_name _;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
# Returns a 404 Not Found for any requests to unhandled domains
|
||||||
|
return 404;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
# Upstream for the API
|
||||||
|
upstream api_server {
|
||||||
|
server app:8000;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Upstream for the Frontend
|
||||||
|
#upstream web_server {
|
||||||
|
# server web:80;
|
||||||
|
#}
|
||||||
|
|
||||||
|
# Server block for your API
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name api.basa.houshan.ai;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
proxy_pass http://api_server;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
# A default server to catch any other requests
|
||||||
|
server {
|
||||||
|
listen 80 default_server;
|
||||||
|
server_name _;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
# Returns a 404 Not Found for any requests to unhandled domains
|
||||||
|
return 404;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
[tool.aerich]
|
||||||
|
tortoise_orm = "settings.TORTOISE_ORM"
|
||||||
|
location = "./migrations"
|
||||||
|
src_folder = "./src"
|
||||||
|
|
@ -0,0 +1,218 @@
|
||||||
|
aerich==0.7.2
|
||||||
|
aioboto3==13.2.0
|
||||||
|
aiobotocore==2.15.2
|
||||||
|
aiofiles==24.1.0
|
||||||
|
aiohappyeyeballs==2.4.3
|
||||||
|
aiohttp==3.10.10
|
||||||
|
aioitertools==0.12.0
|
||||||
|
aiosignal==1.3.1
|
||||||
|
aiosqlite==0.17.0
|
||||||
|
annotated-types==0.7.0
|
||||||
|
anthropic==0.42.0
|
||||||
|
anyio==4.6.0
|
||||||
|
APScheduler==3.11.0
|
||||||
|
asgiref==3.8.1
|
||||||
|
asyncpg==0.29.0
|
||||||
|
attrs==24.2.0
|
||||||
|
backoff==2.2.1
|
||||||
|
bcrypt==4.2.0
|
||||||
|
beautifulsoup4==4.12.3
|
||||||
|
boto3==1.35.36
|
||||||
|
botocore==1.35.36
|
||||||
|
build==1.2.2.post1
|
||||||
|
CacheControl==0.14.2
|
||||||
|
cachetools==5.5.0
|
||||||
|
certifi==2024.8.30
|
||||||
|
cffi==1.17.1
|
||||||
|
chardet==5.2.0
|
||||||
|
charset-normalizer==3.3.2
|
||||||
|
chroma-hnswlib==0.7.3
|
||||||
|
chromadb==0.5.0
|
||||||
|
click==8.1.8
|
||||||
|
coloredlogs==15.0.1
|
||||||
|
cryptography==43.0.1
|
||||||
|
dataclasses-json==0.6.7
|
||||||
|
defusedxml==0.7.1
|
||||||
|
Deprecated==1.2.15
|
||||||
|
dictdiffer==0.9.0
|
||||||
|
distro==1.9.0
|
||||||
|
dnspython==2.6.1
|
||||||
|
docx2txt==0.8
|
||||||
|
duckduckgo_search==7.3.0
|
||||||
|
durationpy==0.9
|
||||||
|
email_validator==2.2.0
|
||||||
|
emoji==2.14.0
|
||||||
|
et_xmlfile==2.0.0
|
||||||
|
eval_type_backport==0.2.2
|
||||||
|
fal_client==0.5.8
|
||||||
|
fastapi==0.115.0
|
||||||
|
fastapi-cli==0.0.5
|
||||||
|
filelock==3.16.1
|
||||||
|
filetype==1.2.0
|
||||||
|
firebase-admin==6.6.0
|
||||||
|
flatbuffers==24.3.25
|
||||||
|
frozenlist==1.4.1
|
||||||
|
fsspec==2024.12.0
|
||||||
|
google-ai-generativelanguage==0.6.10
|
||||||
|
google-api-core==2.21.0
|
||||||
|
google-api-python-client==2.149.0
|
||||||
|
google-auth==2.35.0
|
||||||
|
google-auth-httplib2==0.2.0
|
||||||
|
google-cloud-core==2.4.1
|
||||||
|
google-cloud-firestore==2.20.0
|
||||||
|
google-cloud-storage==2.19.0
|
||||||
|
google-crc32c==1.6.0
|
||||||
|
google-generativeai==0.8.3
|
||||||
|
google-resumable-media==2.7.2
|
||||||
|
googleapis-common-protos==1.65.0
|
||||||
|
greenlet==3.1.1
|
||||||
|
grpcio==1.67.0
|
||||||
|
grpcio-status==1.67.0
|
||||||
|
h11==0.14.0
|
||||||
|
html5lib==1.1
|
||||||
|
httpcore==1.0.5
|
||||||
|
httplib2==0.22.0
|
||||||
|
httptools==0.6.1
|
||||||
|
httpx==0.27.2
|
||||||
|
httpx-sse==0.4.0
|
||||||
|
huggingface-hub==0.27.0
|
||||||
|
humanfriendly==10.0
|
||||||
|
idna==3.10
|
||||||
|
importlib_metadata==8.5.0
|
||||||
|
importlib_resources==6.4.5
|
||||||
|
iso8601==1.1.0
|
||||||
|
isodate==0.7.2
|
||||||
|
Jinja2==3.1.4
|
||||||
|
jiter==0.6.1
|
||||||
|
jmespath==1.0.1
|
||||||
|
joblib==1.4.2
|
||||||
|
jsonpatch==1.33
|
||||||
|
jsonpath-python==1.0.6
|
||||||
|
jsonpointer==3.0.0
|
||||||
|
kavenegar==1.1.2
|
||||||
|
Khayyam==3.0.17
|
||||||
|
kubernetes==31.0.0
|
||||||
|
langchain==0.3.13
|
||||||
|
langchain-anthropic==0.3.1
|
||||||
|
langchain-chroma==0.1.4
|
||||||
|
langchain-community==0.3.13
|
||||||
|
langchain-core==0.3.28
|
||||||
|
langchain-google-genai==2.0.1
|
||||||
|
langchain-openai==0.2.2
|
||||||
|
langchain-text-splitters==0.3.4
|
||||||
|
langdetect==1.0.9
|
||||||
|
langgraph==0.2.37
|
||||||
|
langgraph-checkpoint==2.0.1
|
||||||
|
langgraph-checkpoint-postgres==2.0.1
|
||||||
|
langgraph-sdk==0.1.33
|
||||||
|
langsmith==0.1.135
|
||||||
|
lxml==5.3.0
|
||||||
|
mailersend==0.5.8
|
||||||
|
markdown-it-py==3.0.0
|
||||||
|
MarkupSafe==2.1.5
|
||||||
|
marshmallow==3.23.0
|
||||||
|
mdurl==0.1.2
|
||||||
|
mmh3==5.0.1
|
||||||
|
monotonic==1.6
|
||||||
|
mpmath==1.3.0
|
||||||
|
msgpack==1.1.0
|
||||||
|
multidict==6.1.0
|
||||||
|
mypy-extensions==1.0.0
|
||||||
|
nest-asyncio==1.6.0
|
||||||
|
networkx==3.4.2
|
||||||
|
nltk==3.9.1
|
||||||
|
numpy==1.26.4
|
||||||
|
oauthlib==3.2.2
|
||||||
|
olefile==0.47
|
||||||
|
onnxruntime==1.20.1
|
||||||
|
openai==1.68.2
|
||||||
|
openpyxl==3.1.5
|
||||||
|
opentelemetry-api==1.29.0
|
||||||
|
opentelemetry-exporter-otlp-proto-common==1.29.0
|
||||||
|
opentelemetry-exporter-otlp-proto-grpc==1.29.0
|
||||||
|
opentelemetry-instrumentation==0.50b0
|
||||||
|
opentelemetry-instrumentation-asgi==0.50b0
|
||||||
|
opentelemetry-instrumentation-fastapi==0.50b0
|
||||||
|
opentelemetry-proto==1.29.0
|
||||||
|
opentelemetry-sdk==1.29.0
|
||||||
|
opentelemetry-semantic-conventions==0.50b0
|
||||||
|
opentelemetry-util-http==0.50b0
|
||||||
|
orjson==3.10.7
|
||||||
|
overrides==7.7.0
|
||||||
|
packaging==24.1
|
||||||
|
pandas==2.2.3
|
||||||
|
passlib==1.7.4
|
||||||
|
pillow==11.1.0
|
||||||
|
platformdirs==4.3.6
|
||||||
|
posthog==3.7.4
|
||||||
|
primp==0.11.0
|
||||||
|
propcache==0.2.0
|
||||||
|
proto-plus==1.25.0
|
||||||
|
protobuf==5.28.3
|
||||||
|
psutil==6.1.1
|
||||||
|
psycopg==3.2.3
|
||||||
|
psycopg-pool==3.2.3
|
||||||
|
pyasn1==0.6.1
|
||||||
|
pyasn1_modules==0.4.1
|
||||||
|
pycparser==2.22
|
||||||
|
pydantic==2.9.2
|
||||||
|
pydantic-settings==2.5.2
|
||||||
|
pydantic_core==2.23.4
|
||||||
|
pydub==0.25.1
|
||||||
|
Pygments==2.18.0
|
||||||
|
PyJWT==2.9.0
|
||||||
|
pyparsing==3.2.0
|
||||||
|
pypdf==5.1.0
|
||||||
|
PyPika==0.48.9
|
||||||
|
pypika-tortoise==0.1.6
|
||||||
|
pyproject_hooks==1.2.0
|
||||||
|
python-dateutil==2.9.0.post0
|
||||||
|
python-dotenv==1.0.1
|
||||||
|
python-iso639==2024.10.22
|
||||||
|
python-magic==0.4.27
|
||||||
|
python-multipart==0.0.10
|
||||||
|
python-oxmsg==0.0.1
|
||||||
|
pytz==2024.2
|
||||||
|
PyYAML==6.0.2
|
||||||
|
RapidFuzz==3.11.0
|
||||||
|
regex==2024.9.11
|
||||||
|
requests==2.32.3
|
||||||
|
requests-file==2.1.0
|
||||||
|
requests-oauthlib==2.0.0
|
||||||
|
requests-toolbelt==1.0.0
|
||||||
|
rich==13.8.1
|
||||||
|
rsa==4.9
|
||||||
|
s3transfer==0.10.3
|
||||||
|
shellingham==1.5.4
|
||||||
|
six==1.16.0
|
||||||
|
sniffio==1.3.1
|
||||||
|
soupsieve==2.6
|
||||||
|
SQLAlchemy==2.0.35
|
||||||
|
starlette==0.38.6
|
||||||
|
sympy==1.13.3
|
||||||
|
tenacity==8.5.0
|
||||||
|
tiktoken==0.8.0
|
||||||
|
tokenizers==0.20.3
|
||||||
|
tomlkit==0.13.2
|
||||||
|
tortoise-orm==0.21.6
|
||||||
|
tqdm==4.66.5
|
||||||
|
typer==0.12.5
|
||||||
|
typing-inspect==0.9.0
|
||||||
|
typing_extensions==4.12.2
|
||||||
|
tzdata==2025.1
|
||||||
|
tzlocal==5.2
|
||||||
|
unstructured==0.16.11
|
||||||
|
unstructured-client==0.28.1
|
||||||
|
uritemplate==4.1.1
|
||||||
|
urllib3==2.2.3
|
||||||
|
uuid==1.30
|
||||||
|
uvicorn==0.31.0
|
||||||
|
uvloop==0.20.0
|
||||||
|
watchfiles==0.24.0
|
||||||
|
webencodings==0.5.1
|
||||||
|
websocket-client==1.8.0
|
||||||
|
websockets==13.1
|
||||||
|
wrapt==1.16.0
|
||||||
|
yarl==1.16.0
|
||||||
|
zeep==4.3.1
|
||||||
|
zipp==3.21.0
|
||||||
|
|
@ -0,0 +1,38 @@
|
||||||
|
from os import getenv
|
||||||
|
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||||
|
|
||||||
|
|
||||||
|
class Settings(BaseSettings):
|
||||||
|
base_url: str = getenv("BASE_URL", "")
|
||||||
|
database_url: str = getenv("DATABASE_URL", "")
|
||||||
|
jwt_secret_key: str = getenv("JWT_SECRET_KEY", "")
|
||||||
|
jwt_admin_secret_key: str = getenv("JWT_ADMIN_SECRET_KEY", "")
|
||||||
|
jwt_algorithm: str = getenv("JWT_ALGORITHM", "")
|
||||||
|
kavenegar_api_key: str = getenv("KAVENEGAR_API_KEY", "")
|
||||||
|
openai_api_key: str = getenv("OPENAI_API_KEY", "")
|
||||||
|
google_api_key: str = getenv("GOOGLE_API_KEY", "")
|
||||||
|
deepseek_api_key: str = getenv("DEEPSEEK_API_KEY", "")
|
||||||
|
anthropic_api_key: str = getenv("ANTHROPIC_API_KEY", "")
|
||||||
|
st_endpoint: str = getenv("ST_ENDPOINT", "")
|
||||||
|
st_access_key: str = getenv("ST_ACCESS_KEY", "")
|
||||||
|
st_secret_key: str = getenv("ST_SECRET_KEY", "")
|
||||||
|
st_bucket_name: str = getenv("ST_BUCKET_NAME", "")
|
||||||
|
ydc_api_key: str = getenv("YDC_API_KEY", "")
|
||||||
|
mmerchant_id: str = getenv("MMERCHANT_ID", "")
|
||||||
|
chroma_url: str = getenv("CHROMA_URL", "")
|
||||||
|
chroma_credentials: str = getenv("CHROMA_CREDENTIALS", "")
|
||||||
|
cafebazaar_pishkhan_api_secret: str = getenv("CAFEBAZAAR_PISHKHAN_API_SECRET", "")
|
||||||
|
cafebazaar_price_key: str = getenv("CAFEBAZAAR_PRICE_KEY", "")
|
||||||
|
myket_access_token: str = getenv("MYKET_ACCESS_TOKEN", "")
|
||||||
|
ghost_id: str = getenv("GHOST_ID", "")
|
||||||
|
fal_key: str = getenv("FAL_KEY", "")
|
||||||
|
mailersend_api_key: str = getenv("MAILERSEND_API_KEY", "")
|
||||||
|
model_config = SettingsConfigDict(env_file=".api.env")
|
||||||
|
suno_api_key: str = getenv("SUNO_API_KEY", "")
|
||||||
|
pixverse_api_key: str = getenv("PIXVERSE_API_KEY", "")
|
||||||
|
internal_telegram_secret: str = getenv("INTERNAL_TELEGRAM_SECRET", "")
|
||||||
|
liara_api_url: str = getenv("LIARA_API_URL", "")
|
||||||
|
liara_api_key: str = getenv("LIARA_API_KEY", "")
|
||||||
|
|
||||||
|
|
||||||
|
settings = Settings()
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
from fastapi.security import HTTPBearer
|
||||||
|
from passlib.context import CryptContext
|
||||||
|
|
||||||
|
|
||||||
|
auth_scheme = HTTPBearer(auto_error=True)
|
||||||
|
|
||||||
|
pwd_context = CryptContext(schemes=["bcrypt"])
|
||||||
|
|
@ -0,0 +1,252 @@
|
||||||
|
from fastapi import FastAPI, Form # <-- Form is imported here
|
||||||
|
from fastapi.staticfiles import StaticFiles
|
||||||
|
from fastapi.responses import StreamingResponse, JSONResponse
|
||||||
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
|
from tortoise.contrib.fastapi import register_tortoise
|
||||||
|
from apscheduler.schedulers.asyncio import AsyncIOScheduler
|
||||||
|
from apscheduler.triggers.cron import CronTrigger
|
||||||
|
from .config import settings
|
||||||
|
from .tasks.user import free_credit
|
||||||
|
from .services import storage as storage_service, tool as tool_service
|
||||||
|
from .routes import register_routers
|
||||||
|
from .models import SunoTask
|
||||||
|
from pytz import timezone
|
||||||
|
from typing import Annotated # We keep the import but adjust usage to fix the NameError
|
||||||
|
import fal_client
|
||||||
|
import os
|
||||||
|
import httpx
|
||||||
|
import asyncio
|
||||||
|
import json
|
||||||
|
# مدل مورد نظر Fal AI برای تولید ویدئو از متن
|
||||||
|
FAL_VIDEO_MODEL = "fal-ai/pixverse/v3.5/text-to-video"
|
||||||
|
FAL_API_KEY = os.getenv("FAL_KEY") # کلید Fal API از محیط
|
||||||
|
FAL_BASE_URL = "https://queue.fal.run/fal-ai/pixverse"
|
||||||
|
|
||||||
|
app = FastAPI()
|
||||||
|
|
||||||
|
app.add_middleware(
|
||||||
|
CORSMiddleware,
|
||||||
|
allow_origins=["*"],
|
||||||
|
allow_methods=["*"],
|
||||||
|
allow_headers=["*"],
|
||||||
|
allow_credentials=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# Create scheduler
|
||||||
|
scheduler = AsyncIOScheduler()
|
||||||
|
|
||||||
|
# Add a job to the scheduler
|
||||||
|
tehran_tz = timezone('Asia/Tehran')
|
||||||
|
|
||||||
|
# scheduler.add_job(
|
||||||
|
# free_credit,
|
||||||
|
# CronTrigger(hour=0, minute=0, second=0, timezone=tehran_tz)
|
||||||
|
# )
|
||||||
|
|
||||||
|
# Start the scheduler
|
||||||
|
scheduler.start()
|
||||||
|
|
||||||
|
register_tortoise(
|
||||||
|
app,
|
||||||
|
db_url=settings.database_url,
|
||||||
|
modules={"models": ["src.models"]},
|
||||||
|
generate_schemas=False,
|
||||||
|
add_exception_handlers=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
app.mount(
|
||||||
|
"/.well-known",
|
||||||
|
StaticFiles(directory="static/.well-known"),
|
||||||
|
name=".well-known",
|
||||||
|
)
|
||||||
|
|
||||||
|
register_routers(app)
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/", tags=["home"])
|
||||||
|
async def info():
|
||||||
|
return {
|
||||||
|
"image": "https://www.instagram.com/houshan.ai/",
|
||||||
|
"audio": "https://www.instagram.com/houshan.ai/",
|
||||||
|
"video": "https://www.instagram.com/houshan.ai/",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/apk", tags=["app"])
|
||||||
|
async def apk():
|
||||||
|
file_size = await storage_service.get_file_size("apk/app-release.apk")
|
||||||
|
|
||||||
|
headers = {
|
||||||
|
"Content-Disposition": 'attachment; filename="houshan.apk"',
|
||||||
|
"Content-Length": str(file_size),
|
||||||
|
}
|
||||||
|
|
||||||
|
return StreamingResponse(
|
||||||
|
storage_service.get("apk/app-release.apk"),
|
||||||
|
media_type="application/octet-stream",
|
||||||
|
headers=headers,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ----------------------------------------------------------------------
|
||||||
|
## 🎥 روت عمومی: تولید ویدئو از متن (با استفاده مستقیم از fal_client)
|
||||||
|
# ----------------------------------------------------------------------
|
||||||
|
@app.post("/public/generate-video", tags=["public"])
|
||||||
|
async def generate_public_video(
|
||||||
|
prompt: str = Form(..., description="The text prompt to generate the video from."),
|
||||||
|
length: int = Form(5),
|
||||||
|
negative_prompt: str = Form("blurry, low quality"),
|
||||||
|
):
|
||||||
|
if not FAL_API_KEY:
|
||||||
|
return JSONResponse({
|
||||||
|
"status": "error",
|
||||||
|
"message": "FAL_KEY environment variable is not set."
|
||||||
|
}, 500)
|
||||||
|
|
||||||
|
headers = {
|
||||||
|
"Authorization": f"Key {FAL_API_KEY}",
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
}
|
||||||
|
|
||||||
|
# مرحله ۱: ارسال درخواست تولید ویدیو
|
||||||
|
async with httpx.AsyncClient(timeout=60.0) as client:
|
||||||
|
try:
|
||||||
|
submit_url = f"{FAL_BASE_URL}/v3.5/text-to-video"
|
||||||
|
response = await client.post(
|
||||||
|
submit_url,
|
||||||
|
headers=headers,
|
||||||
|
json={
|
||||||
|
"prompt": prompt,
|
||||||
|
"duration": "5",
|
||||||
|
"resolution":"540p",
|
||||||
|
"negative_prompt": negative_prompt
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
if response.status_code != 200:
|
||||||
|
return JSONResponse({
|
||||||
|
"status": "error",
|
||||||
|
"message": "Failed to submit video generation request.",
|
||||||
|
"details": response.text
|
||||||
|
}, response.status_code)
|
||||||
|
|
||||||
|
data = response.json()
|
||||||
|
request_id = data.get("request_id")
|
||||||
|
|
||||||
|
if not request_id:
|
||||||
|
return JSONResponse({
|
||||||
|
"status": "error",
|
||||||
|
"message": "No request_id returned from Fal API.",
|
||||||
|
"details": data
|
||||||
|
}, 500)
|
||||||
|
|
||||||
|
print(f"✅ Fal request submitted. Request ID: {request_id}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
return JSONResponse({
|
||||||
|
"status": "error",
|
||||||
|
"message": "Failed to connect to Fal API.",
|
||||||
|
"details": str(e)
|
||||||
|
}, 500)
|
||||||
|
|
||||||
|
# مرحله ۲: بررسی وضعیت تا زمان تکمیل
|
||||||
|
status_url = f"{FAL_BASE_URL}/requests/{request_id}/status"
|
||||||
|
for attempt in range(30):
|
||||||
|
await asyncio.sleep(3)
|
||||||
|
async with httpx.AsyncClient(timeout=60.0) as client:
|
||||||
|
status_response = await client.get(status_url, headers=headers)
|
||||||
|
status_data = status_response.json()
|
||||||
|
status = status_data.get("status")
|
||||||
|
|
||||||
|
print(f"🔄 Fal Status Check {attempt+1}: {status}")
|
||||||
|
|
||||||
|
if status == "COMPLETED":
|
||||||
|
break
|
||||||
|
elif status in ["FAILED", "ERROR"]:
|
||||||
|
return JSONResponse({
|
||||||
|
"status": "error",
|
||||||
|
"message": "Video generation failed.",
|
||||||
|
"details": status_data
|
||||||
|
}, 500)
|
||||||
|
else:
|
||||||
|
return JSONResponse({
|
||||||
|
"status": "error",
|
||||||
|
"message": "Timeout: Video not completed in expected time."
|
||||||
|
}, 504)
|
||||||
|
|
||||||
|
# مرحله ۳: دریافت نتیجه نهایی
|
||||||
|
result_url = f"{FAL_BASE_URL}/requests/{request_id}"
|
||||||
|
async with httpx.AsyncClient(timeout=60.0) as client:
|
||||||
|
final_response = await client.get(result_url, headers=headers)
|
||||||
|
|
||||||
|
if final_response.status_code != 200:
|
||||||
|
return JSONResponse({
|
||||||
|
"status": "error",
|
||||||
|
"message": "Failed to retrieve final video result.",
|
||||||
|
"details": final_response.text
|
||||||
|
}, final_response.status_code)
|
||||||
|
|
||||||
|
final_data = final_response.json()
|
||||||
|
|
||||||
|
return JSONResponse({
|
||||||
|
"status": "success",
|
||||||
|
"message": "Video generated successfully.",
|
||||||
|
"video_result": final_data
|
||||||
|
})
|
||||||
|
|
||||||
|
@app.post("/suno/callback")
|
||||||
|
async def suno_callback(payload: dict):
|
||||||
|
print("=== Suno Callback Received ===")
|
||||||
|
print("Payload:", json.dumps(payload, ensure_ascii=False, indent=2))
|
||||||
|
|
||||||
|
task_id = payload.get("data", {}).get("task_id")
|
||||||
|
callback_type = payload.get("data", {}).get("callbackType")
|
||||||
|
|
||||||
|
if not task_id:
|
||||||
|
print("WARNING: task_id missing in callback")
|
||||||
|
return {"ok": True}
|
||||||
|
|
||||||
|
suno_task = await SunoTask.get_or_none(task_id=task_id)
|
||||||
|
if not suno_task:
|
||||||
|
print(f"WARNING: Task {task_id} not found in DB")
|
||||||
|
return {"ok": True}
|
||||||
|
|
||||||
|
suno_task.raw_callback = payload
|
||||||
|
|
||||||
|
if callback_type in ("text", "first"):
|
||||||
|
suno_task.status = "PROCESSING"
|
||||||
|
print(f"Task {task_id} status updated to PROCESSING")
|
||||||
|
|
||||||
|
elif callback_type == "complete":
|
||||||
|
items = payload.get("data", {}).get("data", [])
|
||||||
|
if items:
|
||||||
|
suno_task.audio_url = items[0].get("audio_url")
|
||||||
|
suno_task.image_url = items[0].get("image_url")
|
||||||
|
print(f"Task {task_id} audio_url: {suno_task.audio_url}")
|
||||||
|
print(f"Task {task_id} image_url: {suno_task.image_url}")
|
||||||
|
|
||||||
|
suno_task.status = "COMPLETE"
|
||||||
|
print(f"Task {task_id} status updated to COMPLETE")
|
||||||
|
|
||||||
|
await suno_task.save()
|
||||||
|
print(f"Task {task_id} saved to DB")
|
||||||
|
return {"ok": True}
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/api/suno/tasks/{task_id}")
|
||||||
|
async def get_suno_task(task_id: str):
|
||||||
|
suno_task = await SunoTask.get_or_none(task_id=task_id)
|
||||||
|
|
||||||
|
if not suno_task:
|
||||||
|
print(f"Task {task_id} not found")
|
||||||
|
raise HTTPException(404, "Task not found")
|
||||||
|
|
||||||
|
result = {
|
||||||
|
"task_id": suno_task.task_id,
|
||||||
|
"status": suno_task.status,
|
||||||
|
"audio_url": suno_task.audio_url,
|
||||||
|
"image_url": suno_task.image_url,
|
||||||
|
}
|
||||||
|
print(f"Returning task {task_id}:", json.dumps(result, ensure_ascii=False))
|
||||||
|
return result
|
||||||
|
|
@ -0,0 +1,46 @@
|
||||||
|
MESSAGES = {
|
||||||
|
"user": {
|
||||||
|
"not_found": """شماره وارد شده یافت نشد. لطفاً با شمارهای که در باسا ثبتنام کردهاید وارد شوید.
|
||||||
|
اگر عضو زیرمجموعه خانواده هستید، لطفاً دقیقاً همان شمارهای را وارد کنید که پیامک دعوت برای آن ارسال شده است.""",
|
||||||
|
"wrong_pass": "رمز عبور اشتباه است",
|
||||||
|
"welcome": ["به هوشان خوش اومدی!", "50 سکه هوشان، بسته اعتبار هدیه به شما"],
|
||||||
|
"free": ["هدیه امروز", "سکه هوشان؛ هدیه امروز شما"],
|
||||||
|
"code": ["کد معرف", "20 سکه هوشان رایگان، بابت وارد کردن کد معرف"],
|
||||||
|
"invite": [
|
||||||
|
"دعوت از دوستان",
|
||||||
|
"20 سکه هوشان رایگان، بابت استفاده دیگران از کد معرف شما",
|
||||||
|
],
|
||||||
|
"wrong_input": "لطفا یک شماره موبایل یا ایمیل معتبر وارد نمایید",
|
||||||
|
"try_later": lambda index: f"لطفا بعد از {index} ثانیه دوباره تلاش کنید",
|
||||||
|
},
|
||||||
|
"chat": {
|
||||||
|
"no_credit": "موجودی حساب کافی نیست",
|
||||||
|
"not_found": "چت مورد نظر یافت نشد",
|
||||||
|
"deleted": "چت با موفقیت حذف شد",
|
||||||
|
"limit": "شما به حد محدودیت پیام زمانی رسیدهاید",
|
||||||
|
},
|
||||||
|
"message": {
|
||||||
|
"not_found": "پیام مورد نظر یافت نشد",
|
||||||
|
},
|
||||||
|
"bot": {
|
||||||
|
"not_found": "ربات مورد نظر یافت نشد",
|
||||||
|
},
|
||||||
|
"assistant": {
|
||||||
|
"confirmed": "وضعیت دستیار به تایید شده تغییر یافت",
|
||||||
|
"rejected": "وضعیت دستیار به رد شده تغییر یافت",
|
||||||
|
},
|
||||||
|
"code": {
|
||||||
|
"not_valid": "کد وارد شده معتبر نمیباشد",
|
||||||
|
"not_allow": "شما قبلا کد معرف ثبت کردهاید",
|
||||||
|
},
|
||||||
|
"discount": {
|
||||||
|
"not_allow": "شما قبلا از این کد تخفیف استفاده کردهاید",
|
||||||
|
"not_found": "کد تخفیف وارد شده معتبر نمیباشد",
|
||||||
|
"expired": "کد تخفیف منقضی شده است",
|
||||||
|
},
|
||||||
|
"settlement": {
|
||||||
|
"success": "درخواست تسویه حساب شما با موفقیت ثبت شد. نتیجه از طریق پیامک به شما اطلاعرسانی خواهد شد",
|
||||||
|
"failed": "درآمد شما برای تسویه حساب کافی نیست",
|
||||||
|
},
|
||||||
|
"comment": {"not_allow": "لطفا قبل از ثبت نظر، اقدام به استفاده از بات نمایید"},
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
from .app_metadata import AppMetadata
|
||||||
|
from .banner import Banner
|
||||||
|
from .billing import Billing, BillingStatus, BillingType
|
||||||
|
from .bot import Bot, BotStatus, BotType
|
||||||
|
from .category import Category
|
||||||
|
from .chatbot import Chatbot
|
||||||
|
from .code import Code
|
||||||
|
from .comment import Comment
|
||||||
|
from .discount import Discount
|
||||||
|
from .event import Event
|
||||||
|
from .feedback import Feedback, FeedbackStatus
|
||||||
|
from .mark import Mark
|
||||||
|
from .message import Message
|
||||||
|
from .notification import Notification
|
||||||
|
from .plan import Plan
|
||||||
|
from .rate import Rate
|
||||||
|
from .settlement import Settlement, SettlementType
|
||||||
|
from .suno_task import SunoTask
|
||||||
|
from .ticket import Ticket, TicketRole
|
||||||
|
from .user import User, UserRole
|
||||||
|
from .winner import Winner
|
||||||
|
from .advertisement import Advertisement
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
from tortoise import fields
|
||||||
|
from tortoise.models import Model
|
||||||
|
from .timestamp import Timestamp
|
||||||
|
|
||||||
|
|
||||||
|
class Advertisement(Model, Timestamp):
|
||||||
|
id = fields.IntField(pk=True)
|
||||||
|
user = fields.ForeignKeyField(
|
||||||
|
"models.User", related_name="ads", on_delete=fields.CASCADE
|
||||||
|
)
|
||||||
|
ad_type = fields.CharField(max_length=255)
|
||||||
|
timestamp = fields.DatetimeField(auto_now=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
table = "advertisements"
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
from tortoise import fields
|
||||||
|
from tortoise.models import Model
|
||||||
|
from .timestamp import Timestamp
|
||||||
|
|
||||||
|
|
||||||
|
class AppMetadata(Model, Timestamp):
|
||||||
|
tag = fields.CharField(max_length=24, pk=True)
|
||||||
|
value = fields.TextField(null=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
table = "app_metadata"
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
from tortoise import fields
|
||||||
|
from tortoise.models import Model
|
||||||
|
from .timestamp import Timestamp
|
||||||
|
|
||||||
|
class Banner(Model, Timestamp):
|
||||||
|
id = fields.IntField(pk=True)
|
||||||
|
image = fields.CharField(max_length=255)
|
||||||
|
link = fields.CharField(max_length=255, null=True)
|
||||||
|
title = fields.CharField(max_length=255, default="")
|
||||||
|
description = fields.CharField(max_length=255, default="")
|
||||||
|
bot = fields.ForeignKeyField(
|
||||||
|
"models.Bot", related_name="banners", on_delete=fields.SET_NULL, null=True
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
table = "banners"
|
||||||
|
|
@ -0,0 +1,39 @@
|
||||||
|
from tortoise import fields
|
||||||
|
from tortoise.models import Model
|
||||||
|
from .timestamp import Timestamp
|
||||||
|
from enum import Enum
|
||||||
|
|
||||||
|
|
||||||
|
class BillingStatus(str, Enum):
|
||||||
|
SUCCESS = "success"
|
||||||
|
FAILED = "failed"
|
||||||
|
PENDING = "pending"
|
||||||
|
START ="start"
|
||||||
|
|
||||||
|
|
||||||
|
class BillingType(str, Enum):
|
||||||
|
FREE = "free"
|
||||||
|
CODE = "code"
|
||||||
|
INVITE = "invite"
|
||||||
|
WELCOME = "welcome"
|
||||||
|
PURCHASE = "purchase"
|
||||||
|
INSTAGRAM = "instagram"
|
||||||
|
BASA = "basa"
|
||||||
|
|
||||||
|
|
||||||
|
class Billing(Model, Timestamp):
|
||||||
|
id = fields.IntField(pk=True)
|
||||||
|
amount = fields.IntField()
|
||||||
|
credit = fields.IntField()
|
||||||
|
discount = fields.IntField(default=0)
|
||||||
|
code = fields.CharField(max_length=8, null=True)
|
||||||
|
authority = fields.CharField(max_length=255, null=True, unique=True)
|
||||||
|
ref_id = fields.CharField(max_length=255, null=True, unique=True)
|
||||||
|
type = fields.CharEnumField(BillingType, default=BillingType.PURCHASE)
|
||||||
|
status = fields.CharEnumField(BillingStatus, default=BillingStatus.PENDING)
|
||||||
|
user = fields.ForeignKeyField(
|
||||||
|
"models.User", related_name="billings", on_delete=fields.SET_NULL, null=True
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
table = "billings"
|
||||||
|
|
@ -0,0 +1,62 @@
|
||||||
|
from tortoise import fields
|
||||||
|
from tortoise.models import Model
|
||||||
|
from .timestamp import Timestamp
|
||||||
|
from enum import Enum
|
||||||
|
|
||||||
|
|
||||||
|
class BotStatus(str, Enum):
|
||||||
|
CONFIRMED = "confirmed"
|
||||||
|
REJECTED = "rejected"
|
||||||
|
PENDING = "pending"
|
||||||
|
|
||||||
|
|
||||||
|
class BotType(str, Enum):
|
||||||
|
LLM = "llm"
|
||||||
|
CHARACTER = "character"
|
||||||
|
IMAGE = "image"
|
||||||
|
VIDEO = "video"
|
||||||
|
AUDIO = "audio"
|
||||||
|
|
||||||
|
|
||||||
|
class Bot(Model, Timestamp):
|
||||||
|
id = fields.IntField(pk=True)
|
||||||
|
name = fields.CharField(max_length=255, null=True)
|
||||||
|
model = fields.CharField(max_length=255)
|
||||||
|
style = fields.CharField(max_length=255, null=True)
|
||||||
|
description = fields.TextField(null=True)
|
||||||
|
image = fields.CharField(max_length=255, null=True)
|
||||||
|
image_2 = fields.CharField(max_length=255, null=True)
|
||||||
|
attachment = fields.IntField(default=0)
|
||||||
|
attachment_type = fields.JSONField(null=True)
|
||||||
|
tool = fields.BooleanField(default=False)
|
||||||
|
public = fields.BooleanField(default=True)
|
||||||
|
cost = fields.IntField(default=0)
|
||||||
|
limit = fields.IntField(default=-1)
|
||||||
|
order = fields.IntField(default=1)
|
||||||
|
prompt = fields.TextField(null=True)
|
||||||
|
links = fields.JSONField(null=True)
|
||||||
|
docs = fields.JSONField(null=True)
|
||||||
|
deleted = fields.BooleanField(default=False)
|
||||||
|
web_search = fields.BooleanField(default=False)
|
||||||
|
status = fields.CharEnumField(BotStatus, default=BotStatus.CONFIRMED)
|
||||||
|
type = fields.CharEnumField(BotType, default=BotType.LLM)
|
||||||
|
chatbots = fields.ReverseRelation["Chatbot"]
|
||||||
|
marks = fields.ReverseRelation["Mark"]
|
||||||
|
rates = fields.ReverseRelation["Rate"]
|
||||||
|
allowed_levels = fields.JSONField(default=[1])
|
||||||
|
banners = fields.ReverseRelation["Banner"]
|
||||||
|
user = fields.ForeignKeyField(
|
||||||
|
"models.User",
|
||||||
|
related_name="bots",
|
||||||
|
on_delete=fields.SET_NULL,
|
||||||
|
null=True,
|
||||||
|
)
|
||||||
|
category = fields.ForeignKeyField(
|
||||||
|
"models.Category",
|
||||||
|
related_name="bots",
|
||||||
|
on_delete=fields.SET_NULL,
|
||||||
|
null=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
table = "bots"
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
from tortoise import fields
|
||||||
|
from tortoise.models import Model
|
||||||
|
|
||||||
|
|
||||||
|
class Category(Model):
|
||||||
|
id = fields.IntField(pk=True)
|
||||||
|
name = fields.CharField(max_length=255)
|
||||||
|
description = fields.TextField(null=True)
|
||||||
|
tool = fields.BooleanField(default=False)
|
||||||
|
character = fields.BooleanField(default=False)
|
||||||
|
media = fields.BooleanField(default=False)
|
||||||
|
image = fields.CharField(max_length=255, null=True)
|
||||||
|
icon = fields.CharField(max_length=255, null=True)
|
||||||
|
bots = fields.ReverseRelation["Bot"]
|
||||||
|
comments = fields.ReverseRelation["Comment"]
|
||||||
|
user_bots = fields.ReverseRelation["UserBot"]
|
||||||
|
sub_categories = fields.ReverseRelation["Category"]
|
||||||
|
parent_category = fields.ForeignKeyField(
|
||||||
|
"models.Category",
|
||||||
|
related_name="sub_categories",
|
||||||
|
null=True,
|
||||||
|
on_delete=fields.SET_NULL,
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
table = "categories"
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
from tortoise import fields
|
||||||
|
from tortoise.models import Model
|
||||||
|
from .timestamp import Timestamp
|
||||||
|
|
||||||
|
|
||||||
|
class Chatbot(Model, Timestamp):
|
||||||
|
id = fields.IntField(pk=True)
|
||||||
|
title = fields.CharField(max_length=255, null=True)
|
||||||
|
archive = fields.BooleanField(default=False)
|
||||||
|
messages = fields.ReverseRelation["Message"]
|
||||||
|
user = fields.ForeignKeyField(
|
||||||
|
"models.User", related_name="chatbots", on_delete=fields.SET_NULL, null=True
|
||||||
|
)
|
||||||
|
bot = fields.ForeignKeyField(
|
||||||
|
"models.Bot", related_name="chatbots", on_delete=fields.SET_NULL, null=True
|
||||||
|
)
|
||||||
|
is_deleted = fields.BooleanField(default=False)
|
||||||
|
is_ghost = fields.BooleanField(default=False)
|
||||||
|
class Meta:
|
||||||
|
table = "chatbots"
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
from tortoise import fields
|
||||||
|
from tortoise.models import Model
|
||||||
|
from .timestamp import Timestamp
|
||||||
|
|
||||||
|
|
||||||
|
class Code(Model, Timestamp):
|
||||||
|
id = fields.IntField(pk=True)
|
||||||
|
usage_count = fields.IntField(default=0)
|
||||||
|
code = fields.CharField(max_length=6, unique=True)
|
||||||
|
message = fields.CharField(max_length=255, null=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
table = "code"
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
from tortoise import fields
|
||||||
|
from tortoise.models import Model
|
||||||
|
from .timestamp import Timestamp
|
||||||
|
|
||||||
|
|
||||||
|
class Comment(Model, Timestamp):
|
||||||
|
id = fields.IntField(pk=True)
|
||||||
|
text = fields.TextField()
|
||||||
|
image = fields.CharField(max_length=255, null=True)
|
||||||
|
replies = fields.ReverseRelation["Comment"]
|
||||||
|
feedbacks = fields.ReverseRelation["Feedback"]
|
||||||
|
user = fields.ForeignKeyField(
|
||||||
|
"models.User", related_name="comments", on_delete=fields.CASCADE
|
||||||
|
)
|
||||||
|
category = fields.ForeignKeyField(
|
||||||
|
"models.Category", related_name="comments", on_delete=fields.CASCADE
|
||||||
|
)
|
||||||
|
parent = fields.ForeignKeyField(
|
||||||
|
"models.Comment",
|
||||||
|
related_name="replies",
|
||||||
|
null=True,
|
||||||
|
on_delete=fields.SET_NULL,
|
||||||
|
)
|
||||||
|
replied_user = fields.ForeignKeyField(
|
||||||
|
"models.User",
|
||||||
|
related_name="replied_comments",
|
||||||
|
null=True,
|
||||||
|
on_delete=fields.SET_NULL,
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
table = "comments"
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
from tortoise import fields
|
||||||
|
from tortoise.models import Model
|
||||||
|
from .timestamp import Timestamp
|
||||||
|
|
||||||
|
|
||||||
|
class Discount(Model, Timestamp):
|
||||||
|
id = fields.IntField(pk=True)
|
||||||
|
code = fields.CharField(max_length=8)
|
||||||
|
percent = fields.IntField(null=True)
|
||||||
|
value = fields.IntField(null=True)
|
||||||
|
max_value = fields.IntField(null=True)
|
||||||
|
expires_at = fields.DatetimeField(null=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
table = "discounts"
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
from tortoise import fields
|
||||||
|
from tortoise.models import Model
|
||||||
|
from .timestamp import Timestamp
|
||||||
|
|
||||||
|
|
||||||
|
class Event(Model, Timestamp):
|
||||||
|
id = fields.IntField(pk=True)
|
||||||
|
title = fields.CharField(max_length=255)
|
||||||
|
subtitle = fields.CharField(max_length=255, default="")
|
||||||
|
description = fields.TextField()
|
||||||
|
awards = fields.TextField()
|
||||||
|
image = fields.CharField(max_length=255, null=True)
|
||||||
|
start_at = fields.DatetimeField()
|
||||||
|
end_at = fields.DatetimeField()
|
||||||
|
is_open = fields.BooleanField(default=True)
|
||||||
|
total_participants = fields.IntField(default=0)
|
||||||
|
total_received_works = fields.IntField(default=0)
|
||||||
|
winners = fields.ReverseRelation["Winners"]
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
table = "events"
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
from tortoise import fields
|
||||||
|
from tortoise.models import Model
|
||||||
|
from .timestamp import Timestamp
|
||||||
|
from enum import Enum
|
||||||
|
|
||||||
|
|
||||||
|
class FeedbackStatus(str, Enum):
|
||||||
|
LIKED = "liked"
|
||||||
|
DISLIKED = "disliked"
|
||||||
|
|
||||||
|
|
||||||
|
class Feedback(Model, Timestamp):
|
||||||
|
id = fields.IntField(pk=True)
|
||||||
|
status = fields.CharEnumField(FeedbackStatus)
|
||||||
|
user = fields.ForeignKeyField(
|
||||||
|
"models.User", related_name="feedbacks", on_delete=fields.CASCADE
|
||||||
|
)
|
||||||
|
comment = fields.ForeignKeyField(
|
||||||
|
"models.Comment", related_name="feedbacks", on_delete=fields.CASCADE
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
table = "feedbacks"
|
||||||
|
unique_together = ("user_id", "comment_id")
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
from tortoise import fields
|
||||||
|
from tortoise.models import Model
|
||||||
|
from .timestamp import Timestamp
|
||||||
|
|
||||||
|
|
||||||
|
class Mark(Model, Timestamp):
|
||||||
|
id = fields.IntField(pk=True)
|
||||||
|
user = fields.ForeignKeyField(
|
||||||
|
"models.User", related_name="marks", on_delete=fields.CASCADE
|
||||||
|
)
|
||||||
|
bot = fields.ForeignKeyField(
|
||||||
|
"models.Bot", related_name="marks", on_delete=fields.CASCADE
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
table = "marks"
|
||||||
|
unique_together = ("user_id", "bot_id")
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
from tortoise import fields
|
||||||
|
from tortoise.models import Model
|
||||||
|
from .timestamp import Timestamp
|
||||||
|
|
||||||
|
|
||||||
|
class Message(Model, Timestamp):
|
||||||
|
id = fields.IntField(pk=True)
|
||||||
|
input_tokens = fields.IntField()
|
||||||
|
output_tokens = fields.IntField()
|
||||||
|
cost = fields.IntField(default=-1)
|
||||||
|
chatbot = fields.ForeignKeyField(
|
||||||
|
"models.Chatbot", related_name="messages", on_delete=fields.SET_NULL, null=True
|
||||||
|
)
|
||||||
|
cost_from_gift = fields.IntField(default=0)
|
||||||
|
cost_from_credit = fields.IntField(default=0)
|
||||||
|
cost_from_free = fields.IntField(default=0)
|
||||||
|
is_deleted = fields.BooleanField(default=False)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
table = "messages"
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
from tortoise import fields
|
||||||
|
from tortoise.models import Model
|
||||||
|
from .timestamp import Timestamp
|
||||||
|
|
||||||
|
|
||||||
|
class Notification(Model, Timestamp):
|
||||||
|
id = fields.IntField(pk=True)
|
||||||
|
title = fields.CharField(max_length=255)
|
||||||
|
message = fields.TextField()
|
||||||
|
seen = fields.BooleanField(default=False)
|
||||||
|
user = fields.ForeignKeyField(
|
||||||
|
"models.User", related_name="notifications", on_delete=fields.CASCADE
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
table = "notifications"
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
from tortoise import fields
|
||||||
|
from tortoise.models import Model
|
||||||
|
from .timestamp import Timestamp
|
||||||
|
|
||||||
|
|
||||||
|
class Plan(Model, Timestamp):
|
||||||
|
id = fields.CharField(pk=True, max_length=20)
|
||||||
|
price = fields.IntField()
|
||||||
|
coins = fields.IntField()
|
||||||
|
free_coins = fields.IntField()
|
||||||
|
title = fields.CharField(max_length=255)
|
||||||
|
description = fields.CharField(max_length=255, null=True)
|
||||||
|
active = fields.BooleanField(default=False)
|
||||||
|
image = fields.CharField(max_length=255, null=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
table = "plans"
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
from tortoise import fields
|
||||||
|
from tortoise.models import Model
|
||||||
|
from .timestamp import Timestamp
|
||||||
|
|
||||||
|
|
||||||
|
class Rate(Model, Timestamp):
|
||||||
|
id = fields.IntField(pk=True)
|
||||||
|
text = fields.TextField()
|
||||||
|
score = fields.FloatField()
|
||||||
|
user = fields.ForeignKeyField(
|
||||||
|
"models.User", related_name="rates", on_delete=fields.CASCADE
|
||||||
|
)
|
||||||
|
bot = fields.ForeignKeyField(
|
||||||
|
"models.Bot", related_name="rates", on_delete=fields.CASCADE
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
table = "rates"
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
from tortoise import fields
|
||||||
|
from tortoise.models import Model
|
||||||
|
from .timestamp import Timestamp
|
||||||
|
from enum import Enum
|
||||||
|
|
||||||
|
|
||||||
|
class SettlementType(str, Enum):
|
||||||
|
COIN = "coin"
|
||||||
|
MONEY = "money"
|
||||||
|
|
||||||
|
|
||||||
|
class Settlement(Model, Timestamp):
|
||||||
|
id = fields.IntField(pk=True)
|
||||||
|
income = fields.IntField()
|
||||||
|
confirmed = fields.BooleanField(default=False)
|
||||||
|
type = fields.CharEnumField(SettlementType)
|
||||||
|
user = fields.ForeignKeyField(
|
||||||
|
"models.User", related_name="settlements", on_delete=fields.CASCADE
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
table = "settlements"
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
from tortoise import fields
|
||||||
|
from tortoise.models import Model
|
||||||
|
from .timestamp import Timestamp
|
||||||
|
|
||||||
|
class SunoTask(Model, Timestamp):
|
||||||
|
id = fields.UUIDField(pk=True)
|
||||||
|
task_id = fields.CharField(max_length=255, unique=True)
|
||||||
|
chatbot = fields.ForeignKeyField("models.Chatbot", related_name="suno_tasks")
|
||||||
|
user = fields.ForeignKeyField("models.User", related_name="suno_tasks")
|
||||||
|
status = fields.CharField(
|
||||||
|
max_length=50, default="PENDING"
|
||||||
|
) # PENDING, SUCCESS, FAILED
|
||||||
|
audio_url = fields.CharField(max_length=500, null=True)
|
||||||
|
image_url = fields.CharField(max_length=500, null=True)
|
||||||
|
audio_url2 = fields.CharField(max_length=500, null=True)
|
||||||
|
image_url2 = fields.CharField(max_length=500, null=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
table = "suno_tasks"
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
from tortoise import fields
|
||||||
|
from tortoise.models import Model
|
||||||
|
from .timestamp import Timestamp
|
||||||
|
from enum import Enum
|
||||||
|
|
||||||
|
|
||||||
|
class TicketRole(str, Enum):
|
||||||
|
USER = "user"
|
||||||
|
ADMIN = "admin"
|
||||||
|
|
||||||
|
|
||||||
|
class Ticket(Model, Timestamp):
|
||||||
|
id = fields.IntField(pk=True)
|
||||||
|
text = fields.TextField(null=True)
|
||||||
|
role = fields.CharEnumField(TicketRole, default=TicketRole.USER)
|
||||||
|
file = fields.CharField(max_length=255, null=True)
|
||||||
|
user = fields.ForeignKeyField(
|
||||||
|
"models.User", related_name="tickets", on_delete=fields.CASCADE
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
table = "tickets"
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
from tortoise import fields
|
||||||
|
|
||||||
|
class Timestamp:
|
||||||
|
created_at = fields.DatetimeField(auto_now_add=True)
|
||||||
|
updated_at = fields.DatetimeField(auto_now=True)
|
||||||
|
|
@ -0,0 +1,51 @@
|
||||||
|
from tortoise import fields
|
||||||
|
from tortoise.models import Model
|
||||||
|
from .timestamp import Timestamp
|
||||||
|
from enum import Enum
|
||||||
|
|
||||||
|
class UserRole(str, Enum):
|
||||||
|
USER = "user"
|
||||||
|
ADMIN = "admin"
|
||||||
|
|
||||||
|
|
||||||
|
class User(Model, Timestamp):
|
||||||
|
id = fields.UUIDField(pk=True)
|
||||||
|
mobile_number = fields.CharField(max_length=11, unique=True, index=True, null=True)
|
||||||
|
password = fields.CharField(max_length=255, null=True)
|
||||||
|
name = fields.CharField(max_length=255, null=True)
|
||||||
|
image = fields.CharField(max_length=255, null=True)
|
||||||
|
email = fields.CharField(max_length=255, unique=True, index=True, null=True)
|
||||||
|
username = fields.CharField(max_length=255, unique=True, index=True, null=True)
|
||||||
|
verified = fields.BooleanField(default=False)
|
||||||
|
otp = fields.CharField(max_length=6, null=True)
|
||||||
|
otp_expires_at = fields.DatetimeField(null=True)
|
||||||
|
code = fields.CharField(max_length=6, null=True)
|
||||||
|
card_number = fields.CharField(max_length=16, null=True)
|
||||||
|
income = fields.FloatField(default=0)
|
||||||
|
credit = fields.IntField(default=0)
|
||||||
|
free_credit = fields.IntField(default=0)
|
||||||
|
gift_credit = fields.IntField(default=0)
|
||||||
|
role = fields.CharEnumField(UserRole, default=UserRole.USER)
|
||||||
|
firebase_token = fields.CharField(max_length=255, null=True)
|
||||||
|
telegram_id = fields.CharField(max_length=20, null=True)
|
||||||
|
chatbots = fields.ReverseRelation["Chatbot"]
|
||||||
|
tickets = fields.ReverseRelation["Ticket"]
|
||||||
|
billings = fields.ReverseRelation["Billing"]
|
||||||
|
comments = fields.ReverseRelation["Comment"]
|
||||||
|
replied_comments = fields.ReverseRelation["Comment"]
|
||||||
|
feedbacks = fields.ReverseRelation["Feedback"]
|
||||||
|
marks = fields.ReverseRelation["Mark"]
|
||||||
|
bots = fields.ReverseRelation["Bot"]
|
||||||
|
rates = fields.ReverseRelation["Rate"]
|
||||||
|
notifications = fields.ReverseRelation["Notification"]
|
||||||
|
settlements = fields.ReverseRelation["Settlement"]
|
||||||
|
level = fields.IntField(default=1)
|
||||||
|
parent = fields.ForeignKeyField(
|
||||||
|
"models.User",
|
||||||
|
related_name="sub_users",
|
||||||
|
on_delete=fields.SET_NULL,
|
||||||
|
null=True
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
table = "users"
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
from tortoise import fields
|
||||||
|
from tortoise.models import Model
|
||||||
|
from .timestamp import Timestamp
|
||||||
|
|
||||||
|
|
||||||
|
class Winner(Model, Timestamp):
|
||||||
|
id = fields.IntField(pk=True)
|
||||||
|
rank = fields.IntField()
|
||||||
|
user = fields.ForeignKeyField(
|
||||||
|
"models.User", related_name="events", on_delete=fields.CASCADE
|
||||||
|
)
|
||||||
|
event = fields.ForeignKeyField(
|
||||||
|
"models.Event", related_name="winners", on_delete=fields.CASCADE
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
table = "winners"
|
||||||
|
|
@ -0,0 +1,43 @@
|
||||||
|
from .chatbot import router as chatbot_router
|
||||||
|
from .bot import router as bot_router, admin_router as bot_admin_router
|
||||||
|
from .category import router as category_router, router_v2 as category_router_v2
|
||||||
|
from .tool import router as tool_router
|
||||||
|
from .ticket import router as ticket_router, admin_router as ticket_admin_router
|
||||||
|
from .forum import router as forum_router
|
||||||
|
from .payment import router as payment_router, router_v2 as payment_router_v2
|
||||||
|
from .discount import router as discount_router
|
||||||
|
from .banner import router as banner_router
|
||||||
|
from .event import router as event_router
|
||||||
|
from .app_metadata import router as app_metadata_router
|
||||||
|
from .user import (
|
||||||
|
router as user_router,
|
||||||
|
admin_router as user_admin_router,
|
||||||
|
router_v2 as user_router_v2,
|
||||||
|
)
|
||||||
|
from .advertisement import router as advertisement_router
|
||||||
|
|
||||||
|
|
||||||
|
def register_routers(app):
|
||||||
|
app.include_router(user_router)
|
||||||
|
app.include_router(chatbot_router)
|
||||||
|
app.include_router(bot_router)
|
||||||
|
app.include_router(category_router)
|
||||||
|
app.include_router(tool_router)
|
||||||
|
app.include_router(ticket_router)
|
||||||
|
app.include_router(forum_router)
|
||||||
|
app.include_router(payment_router)
|
||||||
|
app.include_router(discount_router)
|
||||||
|
app.include_router(banner_router)
|
||||||
|
app.include_router(event_router)
|
||||||
|
app.include_router(app_metadata_router)
|
||||||
|
app.include_router(advertisement_router)
|
||||||
|
|
||||||
|
## Admin ##
|
||||||
|
app.include_router(bot_admin_router)
|
||||||
|
app.include_router(user_admin_router)
|
||||||
|
app.include_router(ticket_admin_router)
|
||||||
|
|
||||||
|
## V2 ##
|
||||||
|
app.include_router(payment_router_v2)
|
||||||
|
app.include_router(user_router_v2)
|
||||||
|
app.include_router(category_router_v2)
|
||||||
|
|
@ -0,0 +1,34 @@
|
||||||
|
from fastapi import APIRouter, Depends, Body
|
||||||
|
from typing import Annotated
|
||||||
|
from fastapi.security import HTTPAuthorizationCredentials
|
||||||
|
from ..dependencies import auth_scheme
|
||||||
|
from ..services import advertisement as ad_service
|
||||||
|
from ..services import auth as auth_service
|
||||||
|
from ..schemas.advertisement import Advertisement as AdvertisementSchema
|
||||||
|
|
||||||
|
|
||||||
|
router = APIRouter(
|
||||||
|
prefix="/advertisement",
|
||||||
|
tags=["advertisement"],
|
||||||
|
responses={404: {"detail": "Not found"}},
|
||||||
|
)
|
||||||
|
|
||||||
|
@router.get("/remaining")
|
||||||
|
async def is_ready(
|
||||||
|
token: Annotated[HTTPAuthorizationCredentials, Depends(auth_scheme)],
|
||||||
|
):
|
||||||
|
user_id = await auth_service.verify_token(token.credentials)
|
||||||
|
seconds_remaining = await ad_service.remaining(user_id)
|
||||||
|
return {
|
||||||
|
"remaining_seconds": seconds_remaining
|
||||||
|
}
|
||||||
|
|
||||||
|
@router.post("/claim")
|
||||||
|
async def claim(
|
||||||
|
token: Annotated[HTTPAuthorizationCredentials, Depends(auth_scheme)],
|
||||||
|
body: AdvertisementSchema = Body(...)
|
||||||
|
):
|
||||||
|
user_id = await auth_service.verify_token(token.credentials)
|
||||||
|
response = await ad_service.mark_seen(user_id, body.ad_type)
|
||||||
|
|
||||||
|
return response
|
||||||
|
|
@ -0,0 +1,39 @@
|
||||||
|
from typing import Annotated
|
||||||
|
from fastapi import APIRouter, Depends
|
||||||
|
from fastapi.security import HTTPAuthorizationCredentials
|
||||||
|
from ..dependencies import auth_scheme
|
||||||
|
from ..services import (
|
||||||
|
app_metadata as app_metadata_service,
|
||||||
|
auth as auth_service,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
router = APIRouter(
|
||||||
|
prefix="/app-metadata",
|
||||||
|
tags=["app-metadata"],
|
||||||
|
responses={404: {"detail": "Not found"}},
|
||||||
|
)
|
||||||
|
|
||||||
|
@router.get("")
|
||||||
|
async def findall_metadata():
|
||||||
|
result = await app_metadata_service.findall()
|
||||||
|
return result
|
||||||
|
|
||||||
|
@router.post("")
|
||||||
|
async def upsert_metadata(
|
||||||
|
token: Annotated[HTTPAuthorizationCredentials, Depends(auth_scheme)],
|
||||||
|
tag: str,
|
||||||
|
value: str | None = None,
|
||||||
|
):
|
||||||
|
await auth_service.verify_admin_token(token.credentials)
|
||||||
|
result = await app_metadata_service.upsert(tag, value)
|
||||||
|
return result
|
||||||
|
|
||||||
|
@router.delete("")
|
||||||
|
async def delete_metadata(
|
||||||
|
token: Annotated[HTTPAuthorizationCredentials, Depends(auth_scheme)],
|
||||||
|
tag: str,
|
||||||
|
):
|
||||||
|
await auth_service.verify_admin_token(token.credentials)
|
||||||
|
result = await app_metadata_service.delete(tag)
|
||||||
|
return result
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
from fastapi import APIRouter
|
||||||
|
from fastapi.responses import StreamingResponse
|
||||||
|
from ..schemas.banner import Banners
|
||||||
|
from ..services import banner as banner_service, storage as storage_service
|
||||||
|
|
||||||
|
router = APIRouter(
|
||||||
|
prefix="/banner",
|
||||||
|
tags=["banner"],
|
||||||
|
responses={404: {"detail": "Not found"}},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/", response_model=Banners)
|
||||||
|
async def get_all():
|
||||||
|
banners = await banner_service.get_all()
|
||||||
|
return {"banners": banners}
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/{key}")
|
||||||
|
async def stream_file(
|
||||||
|
key: str,
|
||||||
|
):
|
||||||
|
file_size = await storage_service.get_file_size(f"banner/{key}")
|
||||||
|
headers = {"Content-Length": str(file_size)}
|
||||||
|
|
||||||
|
return StreamingResponse(
|
||||||
|
storage_service.get(f"banner/{key}"),
|
||||||
|
media_type="application/octet-stream",
|
||||||
|
headers=headers,
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,376 @@
|
||||||
|
import tempfile
|
||||||
|
from typing import Annotated
|
||||||
|
from fastapi.responses import StreamingResponse
|
||||||
|
from fastapi import APIRouter, Depends, Form, File, UploadFile
|
||||||
|
from fastapi.security import HTTPAuthorizationCredentials
|
||||||
|
from ..schemas.main import BaseResponse
|
||||||
|
from ..dependencies import auth_scheme
|
||||||
|
from ..messages import MESSAGES
|
||||||
|
from ..services import (
|
||||||
|
bot as bot_service,
|
||||||
|
auth as auth_service,
|
||||||
|
storage as storage_service,
|
||||||
|
)
|
||||||
|
from ..schemas.bot import (
|
||||||
|
Bots,
|
||||||
|
Mark,
|
||||||
|
Categories,
|
||||||
|
MyBots,
|
||||||
|
Comments,
|
||||||
|
BaseComment,
|
||||||
|
OneBot,
|
||||||
|
MyBot,
|
||||||
|
BotStatus,
|
||||||
|
Assistants,
|
||||||
|
OneAssistant,
|
||||||
|
Confirm,
|
||||||
|
Reject,
|
||||||
|
Image,
|
||||||
|
BotLevelUpdate
|
||||||
|
)
|
||||||
|
|
||||||
|
router = APIRouter(
|
||||||
|
prefix="/bot",
|
||||||
|
tags=["bot"],
|
||||||
|
responses={404: {"detail": "Not found"}},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/", response_model=BaseResponse)
|
||||||
|
async def create_bot(
|
||||||
|
token: Annotated[HTTPAuthorizationCredentials, Depends(auth_scheme)],
|
||||||
|
category_id: Annotated[int, Form()],
|
||||||
|
name: Annotated[str, Form()],
|
||||||
|
description: Annotated[str, Form()],
|
||||||
|
prompt: Annotated[str, Form()],
|
||||||
|
public: Annotated[bool, Form()],
|
||||||
|
bot_id: Annotated[int, Form()] = 1,
|
||||||
|
links: Annotated[list[str], Form()] = [],
|
||||||
|
docs: Annotated[list[UploadFile], File()] = [],
|
||||||
|
):
|
||||||
|
user_id = await auth_service.verify_token(token.credentials)
|
||||||
|
|
||||||
|
files = []
|
||||||
|
for doc in docs:
|
||||||
|
mime_type = doc.content_type.split("/")[0]
|
||||||
|
extension = doc.filename.split(".")[-1]
|
||||||
|
|
||||||
|
fil_path = None
|
||||||
|
with tempfile.NamedTemporaryFile(
|
||||||
|
delete=False, suffix=f".{extension}"
|
||||||
|
) as tmp_file:
|
||||||
|
tmp_file.write(await doc.read())
|
||||||
|
fil_path = tmp_file.name
|
||||||
|
|
||||||
|
file_url = None
|
||||||
|
with open(fil_path, "rb") as f:
|
||||||
|
file_url = await storage_service.upload_buffer(
|
||||||
|
user_id=user_id, file=f, mime_type=mime_type, extension=extension
|
||||||
|
)
|
||||||
|
|
||||||
|
files.append([file_url, fil_path])
|
||||||
|
|
||||||
|
await bot_service.create_bot(
|
||||||
|
bot_id=bot_id,
|
||||||
|
user_id=user_id,
|
||||||
|
category_id=category_id,
|
||||||
|
name=name,
|
||||||
|
description=description,
|
||||||
|
prompt=prompt,
|
||||||
|
public=public,
|
||||||
|
links=links,
|
||||||
|
docs=files,
|
||||||
|
)
|
||||||
|
|
||||||
|
return BaseResponse(detail="Bot has been successfully created")
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/", response_model=Bots)
|
||||||
|
async def get_primary_bots(
|
||||||
|
token: Annotated[HTTPAuthorizationCredentials, Depends(auth_scheme)],
|
||||||
|
query: str | None = None,
|
||||||
|
):
|
||||||
|
await auth_service.verify_token(token.credentials)
|
||||||
|
bots = await bot_service.get_primary_bots(query=query)
|
||||||
|
|
||||||
|
return {"bots": [{**bot.__dict__, "category": bot.category} for bot in bots]}
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/personal", response_model=MyBots)
|
||||||
|
async def get_my_bots(
|
||||||
|
token: Annotated[HTTPAuthorizationCredentials, Depends(auth_scheme)],
|
||||||
|
):
|
||||||
|
user_id = await auth_service.verify_token(token.credentials)
|
||||||
|
bots = await bot_service.get_my_bots(user_id=user_id)
|
||||||
|
|
||||||
|
return {"bots": bots}
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/assistant", response_model=Categories)
|
||||||
|
async def get_other_bots(
|
||||||
|
token: Annotated[HTTPAuthorizationCredentials, Depends(auth_scheme)],
|
||||||
|
category: int | None = None,
|
||||||
|
marked: bool | None = None,
|
||||||
|
):
|
||||||
|
user_id = await auth_service.verify_token(token.credentials)
|
||||||
|
result = await bot_service.get_bots_by_message_count(
|
||||||
|
user_id=user_id, category=category, marked=marked
|
||||||
|
)
|
||||||
|
|
||||||
|
return {"categories": result}
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/personal/{id}", response_model=MyBot)
|
||||||
|
async def get_my_bot(
|
||||||
|
token: Annotated[HTTPAuthorizationCredentials, Depends(auth_scheme)], id: int
|
||||||
|
):
|
||||||
|
user_id = await auth_service.verify_token(token.credentials)
|
||||||
|
bot = await bot_service.get_my_bot(id=id, user_id=user_id)
|
||||||
|
|
||||||
|
return {"bot": bot}
|
||||||
|
|
||||||
|
|
||||||
|
@router.put("/personal/{id}", response_model=BaseResponse)
|
||||||
|
async def edit_bot(
|
||||||
|
token: Annotated[HTTPAuthorizationCredentials, Depends(auth_scheme)],
|
||||||
|
id: int,
|
||||||
|
category_id: Annotated[int, Form()],
|
||||||
|
name: Annotated[str, Form()],
|
||||||
|
description: Annotated[str, Form()],
|
||||||
|
prompt: Annotated[str, Form()],
|
||||||
|
public: Annotated[bool, Form()],
|
||||||
|
bot_id: Annotated[int, Form()] = 1,
|
||||||
|
links: Annotated[list[str], Form()] = [],
|
||||||
|
docs: Annotated[list[UploadFile], File()] = [],
|
||||||
|
):
|
||||||
|
user_id = await auth_service.verify_token(token.credentials)
|
||||||
|
|
||||||
|
files = []
|
||||||
|
for doc in docs:
|
||||||
|
mime_type = doc.content_type.split("/")[0]
|
||||||
|
extension = doc.filename.split(".")[-1]
|
||||||
|
|
||||||
|
fil_path = None
|
||||||
|
with tempfile.NamedTemporaryFile(
|
||||||
|
delete=False, suffix=f".{extension}"
|
||||||
|
) as tmp_file:
|
||||||
|
tmp_file.write(await doc.read())
|
||||||
|
fil_path = tmp_file.name
|
||||||
|
|
||||||
|
file_url = None
|
||||||
|
with open(fil_path, "rb") as f:
|
||||||
|
file_url = await storage_service.upload_buffer(
|
||||||
|
user_id=user_id, file=f, mime_type=mime_type, extension=extension
|
||||||
|
)
|
||||||
|
|
||||||
|
files.append([file_url, fil_path])
|
||||||
|
|
||||||
|
await bot_service.edit_bot(
|
||||||
|
id=id,
|
||||||
|
bot_id=bot_id,
|
||||||
|
user_id=user_id,
|
||||||
|
category_id=category_id,
|
||||||
|
name=name,
|
||||||
|
description=description,
|
||||||
|
prompt=prompt,
|
||||||
|
public=public,
|
||||||
|
links=links,
|
||||||
|
docs=files,
|
||||||
|
)
|
||||||
|
|
||||||
|
return BaseResponse(detail="Bot has been successfully edited")
|
||||||
|
|
||||||
|
|
||||||
|
@router.delete("/personal/{id}", response_model=BaseResponse)
|
||||||
|
async def delete_bot(
|
||||||
|
token: Annotated[HTTPAuthorizationCredentials, Depends(auth_scheme)],
|
||||||
|
id: int,
|
||||||
|
):
|
||||||
|
user_id = await auth_service.verify_token(token.credentials)
|
||||||
|
|
||||||
|
await bot_service.delete_bot(id=id, user_id=user_id)
|
||||||
|
|
||||||
|
return BaseResponse(detail="Bot has been successfully deleted")
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/{id}", response_model=OneBot)
|
||||||
|
async def get_bot(
|
||||||
|
token: Annotated[HTTPAuthorizationCredentials, Depends(auth_scheme)],
|
||||||
|
id: int,
|
||||||
|
):
|
||||||
|
user_id = await auth_service.verify_token(token.credentials)
|
||||||
|
bot = await bot_service.get_bot(id=id, user_id=user_id)
|
||||||
|
|
||||||
|
return {"bot": bot}
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/{id}/comment", response_model=Comments)
|
||||||
|
async def get_comments(
|
||||||
|
token: Annotated[HTTPAuthorizationCredentials, Depends(auth_scheme)],
|
||||||
|
id: int,
|
||||||
|
page: int = 1,
|
||||||
|
):
|
||||||
|
await auth_service.verify_token(token.credentials)
|
||||||
|
result = await bot_service.get_comments(id=id, page=page)
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
@router.put("/{id}/comment", response_model=BaseResponse)
|
||||||
|
async def send_comment(
|
||||||
|
token: Annotated[HTTPAuthorizationCredentials, Depends(auth_scheme)],
|
||||||
|
id: int,
|
||||||
|
comment: BaseComment,
|
||||||
|
):
|
||||||
|
user_id = await auth_service.verify_token(token.credentials)
|
||||||
|
await bot_service.send_comment(
|
||||||
|
bot_id=id, user_id=user_id, score=comment.score, text=comment.text
|
||||||
|
)
|
||||||
|
|
||||||
|
return BaseResponse(detail="comment has been successfully registered")
|
||||||
|
|
||||||
|
|
||||||
|
@router.put("/{id}/mark", response_model=BaseResponse)
|
||||||
|
async def mark(
|
||||||
|
token: Annotated[HTTPAuthorizationCredentials, Depends(auth_scheme)],
|
||||||
|
id: int,
|
||||||
|
bot: Mark,
|
||||||
|
):
|
||||||
|
user_id = await auth_service.verify_token(token.credentials)
|
||||||
|
await bot_service.mark(bot_id=id, user_id=user_id, marked=bot.marked)
|
||||||
|
|
||||||
|
return BaseResponse(
|
||||||
|
detail=f"Bot has been successfully {'marked' if bot.marked else 'unmarked'}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/{id}/{key}")
|
||||||
|
async def stream_file(id: str, key: str):
|
||||||
|
file_size = await storage_service.get_file_size(f"bot/{id}/{key}")
|
||||||
|
headers = {"Content-Length": str(file_size)}
|
||||||
|
|
||||||
|
return StreamingResponse(
|
||||||
|
storage_service.get(f"bot/{id}/{key}"),
|
||||||
|
media_type="application/octet-stream",
|
||||||
|
headers=headers,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
##### Admin Routes #####
|
||||||
|
|
||||||
|
admin_router = APIRouter(
|
||||||
|
prefix="/admin/bot",
|
||||||
|
tags=["admin"],
|
||||||
|
responses={404: {"detail": "Not found"}},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@admin_router.get("/assistant", response_model=Assistants)
|
||||||
|
async def get_assistants(
|
||||||
|
token: Annotated[HTTPAuthorizationCredentials, Depends(auth_scheme)],
|
||||||
|
category: int | None = None,
|
||||||
|
q: str | None = None,
|
||||||
|
status: BotStatus = BotStatus.CONFIRMED,
|
||||||
|
page: int = 1,
|
||||||
|
):
|
||||||
|
await auth_service.verify_admin_token(token.credentials)
|
||||||
|
result = await bot_service.get_assistants(
|
||||||
|
category=category, status=status, page=page, q=q
|
||||||
|
)
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
@admin_router.get("/assistant/{id}", response_model=OneAssistant)
|
||||||
|
async def get_assistant(
|
||||||
|
token: Annotated[HTTPAuthorizationCredentials, Depends(auth_scheme)],
|
||||||
|
id: int,
|
||||||
|
):
|
||||||
|
await auth_service.verify_admin_token(token.credentials)
|
||||||
|
assistant = await bot_service.get_assistant(id)
|
||||||
|
|
||||||
|
return {"assistant": assistant}
|
||||||
|
|
||||||
|
|
||||||
|
# @admin_router.put("/assistant/{id}/confirm", response_model=BaseResponse)
|
||||||
|
# async def confirm_assistant(
|
||||||
|
# token: Annotated[HTTPAuthorizationCredentials, Depends(auth_scheme)],
|
||||||
|
# id: int,
|
||||||
|
# assistant: Confirm,
|
||||||
|
# ):
|
||||||
|
# await auth_service.verify_admin_token(token.credentials)
|
||||||
|
# await bot_service.confirm_assistant(
|
||||||
|
# id=id, cost=assistant.cost, message=assistant.message
|
||||||
|
# )
|
||||||
|
|
||||||
|
# return BaseResponse(detail=MESSAGES["assistant"]["confirmed"])
|
||||||
|
|
||||||
|
@admin_router.put("/assistant/{id}/confirm", response_model=BaseResponse)
|
||||||
|
async def confirm_assistant(
|
||||||
|
token: Annotated[HTTPAuthorizationCredentials, Depends(auth_scheme)],
|
||||||
|
id: int,
|
||||||
|
assistant: Confirm,
|
||||||
|
):
|
||||||
|
await auth_service.verify_admin_token(token.credentials)
|
||||||
|
await bot_service.confirm_assistant(
|
||||||
|
id=id,
|
||||||
|
cost=assistant.cost,
|
||||||
|
message=assistant.message,
|
||||||
|
allowed_levels=assistant.allowed_levels # <--- ارسال به سرویس
|
||||||
|
)
|
||||||
|
|
||||||
|
return BaseResponse(detail=MESSAGES["assistant"]["confirmed"])
|
||||||
|
|
||||||
|
|
||||||
|
@admin_router.put("/assistant/{id}/reject", response_model=BaseResponse)
|
||||||
|
async def reject_assistant(
|
||||||
|
token: Annotated[HTTPAuthorizationCredentials, Depends(auth_scheme)],
|
||||||
|
id: int,
|
||||||
|
assistant: Reject,
|
||||||
|
):
|
||||||
|
await auth_service.verify_admin_token(token.credentials)
|
||||||
|
await bot_service.reject_assistant(id=id, message=assistant.message)
|
||||||
|
|
||||||
|
return BaseResponse(detail=MESSAGES["assistant"]["rejected"])
|
||||||
|
|
||||||
|
|
||||||
|
@admin_router.put("/assistant/{id}/image", response_model=Image)
|
||||||
|
async def edit_profile(
|
||||||
|
token: Annotated[HTTPAuthorizationCredentials, Depends(auth_scheme)],
|
||||||
|
file: UploadFile,
|
||||||
|
id: int,
|
||||||
|
):
|
||||||
|
await auth_service.verify_admin_token(token.credentials)
|
||||||
|
url = await bot_service.edit_image(id=id, file=file)
|
||||||
|
|
||||||
|
return {"url": url}
|
||||||
|
|
||||||
|
@admin_router.put("/personal/{id}/levels", response_model=BaseResponse, summary="Update Bot Allowed Levels")
|
||||||
|
async def update_bot_levels(
|
||||||
|
token: Annotated[HTTPAuthorizationCredentials, Depends(auth_scheme)],
|
||||||
|
id: int,
|
||||||
|
data: BotLevelUpdate
|
||||||
|
):
|
||||||
|
|
||||||
|
user_id = await auth_service.verify_token(token.credentials)
|
||||||
|
|
||||||
|
await bot_service.update_allowed_levels(
|
||||||
|
bot_id=id,
|
||||||
|
user_id=user_id,
|
||||||
|
allowed_levels=data.allowed_levels
|
||||||
|
)
|
||||||
|
|
||||||
|
return BaseResponse(detail="Bot levels have been successfully updated")
|
||||||
|
|
||||||
|
@admin_router.delete("/assistant/{id}", response_model=BaseResponse)
|
||||||
|
async def delete_bot(
|
||||||
|
id: int,
|
||||||
|
token: Annotated[HTTPAuthorizationCredentials, Depends(auth_scheme)],
|
||||||
|
):
|
||||||
|
# بررسی دسترسی ادمین
|
||||||
|
await auth_service.verify_admin_token(token.credentials)
|
||||||
|
|
||||||
|
# فراخوانی سرویس
|
||||||
|
await bot_service.delete_bot(id)
|
||||||
|
|
||||||
|
return BaseResponse(detail="بات با موفقیت حذف شد")
|
||||||
|
|
@ -0,0 +1,81 @@
|
||||||
|
from fastapi import APIRouter
|
||||||
|
from ..schemas.category import Categories, ToolCategories
|
||||||
|
from ..services import category as category_service
|
||||||
|
|
||||||
|
router = APIRouter(
|
||||||
|
prefix="/category",
|
||||||
|
tags=["category"],
|
||||||
|
responses={404: {"detail": "Not found"}},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/", response_model=Categories)
|
||||||
|
async def get_categories():
|
||||||
|
categories = await category_service.get_categories()
|
||||||
|
|
||||||
|
return {"categories": categories}
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/tool", response_model=ToolCategories)
|
||||||
|
async def get_tool_categories():
|
||||||
|
categories = await category_service.get_tool_categories()
|
||||||
|
|
||||||
|
return {"categories": categories}
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/character", response_model=ToolCategories)
|
||||||
|
async def get_character_categories():
|
||||||
|
categories = await category_service.get_character_categories()
|
||||||
|
|
||||||
|
return {"categories": categories}
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/media", response_model=Categories)
|
||||||
|
async def get_media_categories():
|
||||||
|
categories = await category_service.get_media_categories()
|
||||||
|
|
||||||
|
return {"categories": categories}
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/media/{id}", response_model=ToolCategories)
|
||||||
|
async def get_media_category(id: int):
|
||||||
|
categories = await category_service.get_media_category(id)
|
||||||
|
|
||||||
|
return {"categories": categories}
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/effect")
|
||||||
|
async def get_video_effects():
|
||||||
|
return {
|
||||||
|
"effects": [
|
||||||
|
{
|
||||||
|
"name": "Hug Your Love",
|
||||||
|
"gif": "https://media.pixverse.ai/asset%2Ftemplate%2Fweb_hugyourlove.gif",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Hot Harley Quinn",
|
||||||
|
"gif": "https://media.pixverse.ai/asset%2Ftemplate%2Fweb_harleyquinn.gif",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Muscle Surge",
|
||||||
|
"gif": "https://media.pixverse.ai/asset%2Ftemplate%2Fweb_musclesurge.gif",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
##### V2 Routes #####
|
||||||
|
|
||||||
|
|
||||||
|
router_v2 = APIRouter(
|
||||||
|
prefix="/v2/category",
|
||||||
|
tags=["category"],
|
||||||
|
responses={404: {"detail": "Not found"}},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router_v2.get("/tool", response_model=ToolCategories)
|
||||||
|
async def get_tool_categories_v2():
|
||||||
|
categories = await category_service.get_tool_categories_v2()
|
||||||
|
|
||||||
|
return {"categories": categories}
|
||||||
|
|
@ -0,0 +1,371 @@
|
||||||
|
import tempfile
|
||||||
|
from fastapi import APIRouter, Depends, UploadFile, Form, File
|
||||||
|
from fastapi.responses import StreamingResponse
|
||||||
|
from fastapi.security import HTTPAuthorizationCredentials
|
||||||
|
from typing import Annotated
|
||||||
|
from datetime import date
|
||||||
|
from langchain_core.messages import HumanMessage, AIMessage
|
||||||
|
from ..config import settings
|
||||||
|
from ..models import BotType , SunoTask
|
||||||
|
from ..dependencies import auth_scheme
|
||||||
|
from ..schemas.main import BaseResponse
|
||||||
|
from ..services import (
|
||||||
|
chatbot as chatbot_service,
|
||||||
|
auth as auth_service,
|
||||||
|
storage as storage_service,
|
||||||
|
)
|
||||||
|
import json
|
||||||
|
from ..schemas.chatbot import (
|
||||||
|
ChatHistory,
|
||||||
|
Like,
|
||||||
|
Chats,
|
||||||
|
Title,
|
||||||
|
BaseMessage,
|
||||||
|
RelatedQuestions,
|
||||||
|
Archive,
|
||||||
|
MessageStream,
|
||||||
|
)
|
||||||
|
|
||||||
|
router = APIRouter(
|
||||||
|
prefix="/chatbot",
|
||||||
|
tags=["chatbot"],
|
||||||
|
responses={404: {"detail": "Not found"}},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/", response_model=MessageStream)
|
||||||
|
async def send_message(
|
||||||
|
token: Annotated[HTTPAuthorizationCredentials, Depends(auth_scheme)],
|
||||||
|
bot_id: Annotated[int, Form()],
|
||||||
|
query: Annotated[str | None, Form()] = None,
|
||||||
|
id: Annotated[int | None, Form()] = None,
|
||||||
|
retry: Annotated[bool, Form()] = False,
|
||||||
|
ghost: Annotated[bool, Form()] = False,
|
||||||
|
image: Annotated[UploadFile | None, File()] = None,
|
||||||
|
audio: Annotated[UploadFile | None, File()] = None,
|
||||||
|
doc: Annotated[UploadFile | None, File()] = None,
|
||||||
|
):
|
||||||
|
user_id = await auth_service.verify_token(token.credentials)
|
||||||
|
chat_user_id = settings.ghost_id if ghost else user_id
|
||||||
|
|
||||||
|
image_url = None
|
||||||
|
if image:
|
||||||
|
image_url = await storage_service.upload(id=chat_user_id, file=image)
|
||||||
|
|
||||||
|
audio_url = None
|
||||||
|
audio_path = None
|
||||||
|
if audio:
|
||||||
|
mime_type = audio.content_type.split("/")[0]
|
||||||
|
extension = audio.filename.split(".")[-1]
|
||||||
|
|
||||||
|
with tempfile.NamedTemporaryFile(
|
||||||
|
delete=False, suffix=f".{extension}"
|
||||||
|
) as tmp_file:
|
||||||
|
tmp_file.write(await audio.read())
|
||||||
|
audio_path = tmp_file.name
|
||||||
|
|
||||||
|
with open(audio_path, "rb") as f:
|
||||||
|
audio_url = await storage_service.upload_buffer(
|
||||||
|
user_id=chat_user_id,
|
||||||
|
file=f,
|
||||||
|
mime_type=mime_type,
|
||||||
|
extension=extension,
|
||||||
|
)
|
||||||
|
|
||||||
|
doc_url = None
|
||||||
|
doc_path = None
|
||||||
|
if doc:
|
||||||
|
mime_type = doc.content_type.split("/")[0]
|
||||||
|
extension = doc.filename.split(".")[-1]
|
||||||
|
|
||||||
|
with tempfile.NamedTemporaryFile(
|
||||||
|
delete=False, suffix=f".{extension}"
|
||||||
|
) as tmp_file:
|
||||||
|
tmp_file.write(await doc.read())
|
||||||
|
doc_path = tmp_file.name
|
||||||
|
|
||||||
|
with open(doc_path, "rb") as f:
|
||||||
|
doc_url = await storage_service.upload_buffer(
|
||||||
|
user_id=chat_user_id, file=f, mime_type=mime_type, extension=extension
|
||||||
|
)
|
||||||
|
|
||||||
|
return StreamingResponse(
|
||||||
|
chatbot_service.send_message(
|
||||||
|
id=id,
|
||||||
|
query=query,
|
||||||
|
bot_id=bot_id,
|
||||||
|
user_id=user_id,
|
||||||
|
retry=retry,
|
||||||
|
ghost=ghost,
|
||||||
|
image=(settings.base_url + image_url) if image_url else None,
|
||||||
|
audio=[
|
||||||
|
(settings.base_url + audio_url) if audio_url else None,
|
||||||
|
audio_path,
|
||||||
|
],
|
||||||
|
doc=[(settings.base_url + doc_url) if doc_url else None, doc_path],
|
||||||
|
),
|
||||||
|
media_type="application/json",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/", response_model=Chats)
|
||||||
|
async def chats_history(
|
||||||
|
token: Annotated[HTTPAuthorizationCredentials, Depends(auth_scheme)],
|
||||||
|
archive: bool = False,
|
||||||
|
query: str | None = None,
|
||||||
|
date: date | None = None,
|
||||||
|
type: BotType | None = None,
|
||||||
|
page: int = 1,
|
||||||
|
):
|
||||||
|
user_id = await auth_service.verify_token(token.credentials)
|
||||||
|
|
||||||
|
response = await chatbot_service.get_chats(
|
||||||
|
user_id=user_id, query=query, archive=archive, date=date, page=page, type=type
|
||||||
|
)
|
||||||
|
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
@router.delete("/", response_model=BaseResponse)
|
||||||
|
async def delete_chats(
|
||||||
|
token: Annotated[HTTPAuthorizationCredentials, Depends(auth_scheme)],
|
||||||
|
chat: Archive,
|
||||||
|
):
|
||||||
|
user_id = await auth_service.verify_token(token.credentials)
|
||||||
|
|
||||||
|
await chatbot_service.delete_chats(user_id=user_id, archive=chat.archive)
|
||||||
|
|
||||||
|
return BaseResponse(detail="Chats deleted successfully")
|
||||||
|
|
||||||
|
|
||||||
|
# @router.get("/{id}", response_model=ChatHistory)
|
||||||
|
# async def chat_history(
|
||||||
|
# token: Annotated[HTTPAuthorizationCredentials, Depends(auth_scheme)],
|
||||||
|
# id: int,
|
||||||
|
# ):
|
||||||
|
# user_id = await auth_service.verify_token(token.credentials)
|
||||||
|
|
||||||
|
# history, chatbot = await chatbot_service.get_chat(id=id, user_id=user_id)
|
||||||
|
# messages = history.checkpoint["channel_values"]["messages"]
|
||||||
|
|
||||||
|
# return {
|
||||||
|
# "id": id,
|
||||||
|
# "title": chatbot.title,
|
||||||
|
# "created_at": chatbot.created_at,
|
||||||
|
# "bot": {**chatbot.bot.__dict__, "category": chatbot.bot.category},
|
||||||
|
# "messages": [
|
||||||
|
# {
|
||||||
|
# "id": message.id,
|
||||||
|
# "content": message.content
|
||||||
|
# if isinstance(message, HumanMessage)
|
||||||
|
# else [
|
||||||
|
# {
|
||||||
|
# "type": "text",
|
||||||
|
# "text": message.content
|
||||||
|
# if not message.content.startswith("https")
|
||||||
|
# else "",
|
||||||
|
# "audio_url": {"url": message.content, "attachment": True}
|
||||||
|
# if ".mp3" in message.content or "erweima.ai" in message.content
|
||||||
|
# else None,
|
||||||
|
# "image_url": {"url": message.content, "attachment": True}
|
||||||
|
# if ".png" in message.content
|
||||||
|
# or ".jpg" in message.content
|
||||||
|
# or ".webp" in message.content
|
||||||
|
# or ".svg" in message.content
|
||||||
|
# else None,
|
||||||
|
# "video_url": {"url": message.content, "attachment": True}
|
||||||
|
# if ".mp4" in message.content
|
||||||
|
# else None,
|
||||||
|
# }
|
||||||
|
# ],
|
||||||
|
# "like": message.additional_kwargs.get("like", None),
|
||||||
|
# "role": "human" if isinstance(message, HumanMessage) else "ai",
|
||||||
|
# "created_at": message.additional_kwargs.get("created_at", None),
|
||||||
|
# }
|
||||||
|
# for message in messages
|
||||||
|
# if isinstance(message, HumanMessage)
|
||||||
|
# or (isinstance(message, AIMessage) and not message.tool_calls)
|
||||||
|
# ],
|
||||||
|
# }
|
||||||
|
|
||||||
|
@router.get("/{id}", response_model=ChatHistory)
|
||||||
|
async def chat_history(
|
||||||
|
token: Annotated[HTTPAuthorizationCredentials, Depends(auth_scheme)],
|
||||||
|
id: int,
|
||||||
|
):
|
||||||
|
print(f"\n========== DEBUG: chat_history START (ID: {id}) ==========")
|
||||||
|
|
||||||
|
# 1. احراز هویت و دریافت اطلاعات چت
|
||||||
|
user_id = await auth_service.verify_token(token.credentials)
|
||||||
|
history, chatbot = await chatbot_service.get_chat(id=id, user_id=user_id)
|
||||||
|
|
||||||
|
# دسترسی به پیامهای خام ذخیره شده در گراف
|
||||||
|
raw_messages = history.checkpoint["channel_values"]["messages"]
|
||||||
|
print(f"[DEBUG] Raw messages count: {len(raw_messages)}")
|
||||||
|
|
||||||
|
formatted_messages = []
|
||||||
|
|
||||||
|
for idx, message in enumerate(raw_messages):
|
||||||
|
# 2. فیلتر کردن پیامهای سیستمی و Tool Callها
|
||||||
|
if not (isinstance(message, HumanMessage) or (isinstance(message, AIMessage) and not message.tool_calls)):
|
||||||
|
# print(f"[DEBUG] Msg #{idx} SKIPPED (Type: {type(message).__name__})")
|
||||||
|
continue
|
||||||
|
|
||||||
|
print(f"[DEBUG] Processing Msg #{idx} | ID: {message.id}")
|
||||||
|
|
||||||
|
# 3. ساخت دیکشنری پایه پیام
|
||||||
|
msg_dict = {
|
||||||
|
"id": message.id,
|
||||||
|
"like": message.additional_kwargs.get("like", None),
|
||||||
|
"role": "human" if isinstance(message, HumanMessage) else "ai",
|
||||||
|
"created_at": message.additional_kwargs.get("created_at", None),
|
||||||
|
}
|
||||||
|
|
||||||
|
# 4. منطق تولید محتوا (Content Logic)
|
||||||
|
if isinstance(message, HumanMessage):
|
||||||
|
# پیام کاربر فقط متن است
|
||||||
|
msg_dict["content"] = message.content
|
||||||
|
else:
|
||||||
|
# === منطق دریافت لینک موزیک/تصویر برای پیام AI ===
|
||||||
|
suno_audio_url = None
|
||||||
|
suno_image_url = None
|
||||||
|
|
||||||
|
# الف) تلاش برای خواندن زنده از جدول SunoTask (اولویت اول)
|
||||||
|
task_id = message.additional_kwargs.get("task_id")
|
||||||
|
if task_id:
|
||||||
|
try:
|
||||||
|
suno_task = await SunoTask.get_or_none(task_id=task_id)
|
||||||
|
if suno_task:
|
||||||
|
suno_audio_url = suno_task.audio_url
|
||||||
|
suno_image_url = suno_task.image_url
|
||||||
|
print(f" [DB-Lookup] Found Task {task_id} -> Audio: {suno_audio_url}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f" [DB-Lookup] Error: {str(e)}")
|
||||||
|
|
||||||
|
# ب) تعیین لینک نهایی با اولویتبندی: 1. دیتابیس 2. متادیتا 3. متن پیام
|
||||||
|
final_audio_url = (
|
||||||
|
suno_audio_url or
|
||||||
|
message.additional_kwargs.get("audio_url") or
|
||||||
|
(message.content if ".mp3" in message.content or "erweima.ai" in message.content else None)
|
||||||
|
)
|
||||||
|
|
||||||
|
final_image_url = (
|
||||||
|
suno_image_url or
|
||||||
|
message.additional_kwargs.get("image_url") or
|
||||||
|
(message.content if any(ext in message.content for ext in [".png", ".jpg", ".webp"]) else None)
|
||||||
|
)
|
||||||
|
|
||||||
|
# ج) بررسی اینکه آیا خود متن پیام فقط یک لینک است؟ (برای تمیز کردن نمایش متن)
|
||||||
|
is_link_text = message.content.startswith("https")
|
||||||
|
|
||||||
|
# د) ساختار نهایی کانتنت برای فرانتاند
|
||||||
|
msg_dict["content"] = [
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
# اگر متن خودِ لینک است، آن را نمایش نده (چون در پلیر نمایش داده میشود)
|
||||||
|
"text": message.content if not is_link_text else "",
|
||||||
|
|
||||||
|
"audio_url": {"url": final_audio_url, "attachment": True}
|
||||||
|
if final_audio_url else None,
|
||||||
|
|
||||||
|
"image_url": {"url": final_image_url, "attachment": True}
|
||||||
|
if final_image_url else None,
|
||||||
|
|
||||||
|
"video_url": {"url": message.content, "attachment": True}
|
||||||
|
if ".mp4" in message.content else None,
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
formatted_messages.append(msg_dict)
|
||||||
|
|
||||||
|
# 5. ساخت پاسخ نهایی
|
||||||
|
response_data = {
|
||||||
|
"id": id,
|
||||||
|
"title": chatbot.title,
|
||||||
|
"created_at": chatbot.created_at,
|
||||||
|
"bot": {**chatbot.bot.__dict__, "category": chatbot.bot.category},
|
||||||
|
"messages": formatted_messages,
|
||||||
|
}
|
||||||
|
|
||||||
|
print("========== DEBUG: chat_history END ==========\n")
|
||||||
|
return response_data
|
||||||
|
|
||||||
|
|
||||||
|
@router.delete("/{id}", response_model=BaseResponse)
|
||||||
|
async def delete_chat(
|
||||||
|
token: Annotated[HTTPAuthorizationCredentials, Depends(auth_scheme)],
|
||||||
|
id: int,
|
||||||
|
):
|
||||||
|
user_id = await auth_service.verify_token(token.credentials)
|
||||||
|
|
||||||
|
await chatbot_service.delete_chat(id=id, user_id=user_id)
|
||||||
|
|
||||||
|
return BaseResponse(detail="Chat deleted successfully")
|
||||||
|
|
||||||
|
|
||||||
|
@router.put("/{id}/title", response_model=BaseResponse)
|
||||||
|
async def edit_title(
|
||||||
|
token: Annotated[HTTPAuthorizationCredentials, Depends(auth_scheme)],
|
||||||
|
id: int,
|
||||||
|
chat: Title,
|
||||||
|
):
|
||||||
|
user_id = await auth_service.verify_token(token.credentials)
|
||||||
|
|
||||||
|
await chatbot_service.edit_title(id=id, user_id=user_id, title=chat.title)
|
||||||
|
|
||||||
|
return BaseResponse(detail="Title edited successfully")
|
||||||
|
|
||||||
|
|
||||||
|
@router.put("/{id}/archive", response_model=BaseResponse)
|
||||||
|
async def archive(
|
||||||
|
token: Annotated[HTTPAuthorizationCredentials, Depends(auth_scheme)],
|
||||||
|
id: int,
|
||||||
|
chat: Archive,
|
||||||
|
):
|
||||||
|
user_id = await auth_service.verify_token(token.credentials)
|
||||||
|
|
||||||
|
await chatbot_service.archive(id=id, user_id=user_id, archive=chat.archive)
|
||||||
|
|
||||||
|
return BaseResponse(detail="Title edited successfully")
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/{id}/related_questions", response_model=RelatedQuestions)
|
||||||
|
async def related_questions(
|
||||||
|
token: Annotated[HTTPAuthorizationCredentials, Depends(auth_scheme)],
|
||||||
|
id: int,
|
||||||
|
message: BaseMessage,
|
||||||
|
):
|
||||||
|
await auth_service.verify_token(token.credentials)
|
||||||
|
|
||||||
|
questions = await chatbot_service.related_questions(query=message.content)
|
||||||
|
|
||||||
|
return {"questions": questions}
|
||||||
|
|
||||||
|
|
||||||
|
@router.delete("/{id}/message/{message_id}", response_model=BaseResponse)
|
||||||
|
async def delete_message(
|
||||||
|
token: Annotated[HTTPAuthorizationCredentials, Depends(auth_scheme)],
|
||||||
|
id: int,
|
||||||
|
message_id: str,
|
||||||
|
):
|
||||||
|
user_id = await auth_service.verify_token(token.credentials)
|
||||||
|
|
||||||
|
await chatbot_service.delete_message(id=id, message_id=message_id, user_id=user_id)
|
||||||
|
|
||||||
|
return BaseResponse(detail="Message deleted successfully")
|
||||||
|
|
||||||
|
|
||||||
|
@router.put("/{id}/message/{message_id}/feedback", response_model=BaseResponse)
|
||||||
|
async def like_message(
|
||||||
|
token: Annotated[HTTPAuthorizationCredentials, Depends(auth_scheme)],
|
||||||
|
id: int,
|
||||||
|
message_id: str,
|
||||||
|
message: Like,
|
||||||
|
):
|
||||||
|
user_id = await auth_service.verify_token(token.credentials)
|
||||||
|
|
||||||
|
await chatbot_service.like_message(
|
||||||
|
id=id, message_id=message_id, user_id=user_id, like=message.like
|
||||||
|
)
|
||||||
|
|
||||||
|
return BaseResponse(detail="Message updated successfully")
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
from fastapi import APIRouter, Depends
|
||||||
|
from fastapi.security import HTTPAuthorizationCredentials
|
||||||
|
from typing import Annotated
|
||||||
|
from ..schemas.discount import Code, Discount
|
||||||
|
from ..dependencies import auth_scheme
|
||||||
|
from ..services import discount as discount_service, auth as auth_service
|
||||||
|
|
||||||
|
router = APIRouter(
|
||||||
|
prefix="/discount",
|
||||||
|
tags=["discount"],
|
||||||
|
responses={404: {"detail": "Not found"}},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/", response_model=Discount)
|
||||||
|
async def verify(
|
||||||
|
token: Annotated[HTTPAuthorizationCredentials, Depends(auth_scheme)],
|
||||||
|
discount: Code,
|
||||||
|
):
|
||||||
|
user_id = await auth_service.verify_token(token.credentials)
|
||||||
|
discount, bazzar_token = await discount_service.verify(
|
||||||
|
user_id=user_id, code=discount.code, product_id=discount.product_id
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"percent": discount.percent,
|
||||||
|
"value": discount.value,
|
||||||
|
"maxValue": discount.max_value,
|
||||||
|
"bazzar_token": bazzar_token,
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
from fastapi import APIRouter, Depends
|
||||||
|
from fastapi.security import HTTPAuthorizationCredentials
|
||||||
|
from typing import Annotated
|
||||||
|
from ..dependencies import auth_scheme
|
||||||
|
from ..services import event as event_service, auth as auth_service
|
||||||
|
|
||||||
|
router = APIRouter(
|
||||||
|
prefix="/event",
|
||||||
|
tags=["event"],
|
||||||
|
responses={404: {"detail": "Not found"}},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/")
|
||||||
|
async def get_all(token: Annotated[HTTPAuthorizationCredentials, Depends(auth_scheme)]):
|
||||||
|
await auth_service.verify_token(token.credentials)
|
||||||
|
events = await event_service.get_all()
|
||||||
|
|
||||||
|
return {"events": events}
|
||||||
|
|
@ -0,0 +1,87 @@
|
||||||
|
from uuid import UUID
|
||||||
|
from fastapi import APIRouter, Depends, UploadFile, Form, File
|
||||||
|
from fastapi.security import HTTPAuthorizationCredentials
|
||||||
|
from typing import Annotated, Literal
|
||||||
|
from ..schemas.main import BaseResponse
|
||||||
|
from ..schemas.comment import NewComment, Comments, Replies, Feedback
|
||||||
|
from ..dependencies import auth_scheme
|
||||||
|
from ..services import (
|
||||||
|
forum as forum_service,
|
||||||
|
auth as auth_service,
|
||||||
|
storage as storage_service,
|
||||||
|
)
|
||||||
|
|
||||||
|
router = APIRouter(
|
||||||
|
prefix="/forum/comment",
|
||||||
|
tags=["forum"],
|
||||||
|
responses={404: {"detail": "Not found"}},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/", response_model=Comments)
|
||||||
|
async def get_comments(
|
||||||
|
token: Annotated[HTTPAuthorizationCredentials, Depends(auth_scheme)],
|
||||||
|
category_id: int,
|
||||||
|
page: int = 1,
|
||||||
|
order_by: Literal["date", "replies"] = "date",
|
||||||
|
):
|
||||||
|
user_id = await auth_service.verify_token(token.credentials)
|
||||||
|
|
||||||
|
result = await forum_service.get_comments(
|
||||||
|
category_id=category_id, user_id=user_id, page=page, order_by=order_by
|
||||||
|
)
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/", response_model=NewComment)
|
||||||
|
async def send_comment(
|
||||||
|
token: Annotated[HTTPAuthorizationCredentials, Depends(auth_scheme)],
|
||||||
|
text: Annotated[str, Form()],
|
||||||
|
category_id: Annotated[int, Form()],
|
||||||
|
parent_id: Annotated[int | None, Form()] = None,
|
||||||
|
replied_user_id: Annotated[UUID | None, Form()] = None,
|
||||||
|
image: Annotated[UploadFile | None, File()] = None,
|
||||||
|
):
|
||||||
|
user_id = await auth_service.verify_token(token.credentials)
|
||||||
|
|
||||||
|
image_url = None
|
||||||
|
if image:
|
||||||
|
image_url = await storage_service.upload(id=user_id, file=image)
|
||||||
|
|
||||||
|
comment = await forum_service.send_comment(
|
||||||
|
text=text,
|
||||||
|
image_url=image_url,
|
||||||
|
user_id=user_id,
|
||||||
|
category_id=category_id,
|
||||||
|
parent_id=parent_id,
|
||||||
|
replied_user_id=replied_user_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
return {"comment": comment}
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/{id}", response_model=Replies)
|
||||||
|
async def get_replies(
|
||||||
|
token: Annotated[HTTPAuthorizationCredentials, Depends(auth_scheme)],
|
||||||
|
id: int,
|
||||||
|
page: int = 1,
|
||||||
|
):
|
||||||
|
user_id = await auth_service.verify_token(token.credentials)
|
||||||
|
|
||||||
|
result = await forum_service.get_replies(parent_id=id, user_id=user_id, page=page)
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
@router.put("/{id}/feedback", response_model=BaseResponse)
|
||||||
|
async def feedback(
|
||||||
|
token: Annotated[HTTPAuthorizationCredentials, Depends(auth_scheme)],
|
||||||
|
id: int,
|
||||||
|
feedback: Feedback,
|
||||||
|
):
|
||||||
|
user_id = await auth_service.verify_token(token.credentials)
|
||||||
|
|
||||||
|
await forum_service.feedback(status=feedback.status, user_id=user_id, comment_id=id)
|
||||||
|
|
||||||
|
return BaseResponse(detail="Feedback has been successfully registered")
|
||||||
|
|
@ -0,0 +1,150 @@
|
||||||
|
from fastapi import APIRouter, Depends
|
||||||
|
from fastapi.security import HTTPAuthorizationCredentials
|
||||||
|
from fastapi.responses import RedirectResponse, StreamingResponse
|
||||||
|
from typing import Annotated
|
||||||
|
|
||||||
|
from ..messages import MESSAGES
|
||||||
|
from ..dependencies import auth_scheme
|
||||||
|
from ..schemas.payment import Billings, Url, Plans, Settlement, Credit, PlansV2 ,BasaCreditRequest
|
||||||
|
from ..services import (
|
||||||
|
payment as payment_service,
|
||||||
|
auth as auth_service,
|
||||||
|
storage as storage_service,
|
||||||
|
)
|
||||||
|
|
||||||
|
router = APIRouter(
|
||||||
|
prefix="/paymant",
|
||||||
|
tags=["paymant"],
|
||||||
|
responses={404: {"detail": "Not found"}},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/", response_model=Url)
|
||||||
|
async def request(
|
||||||
|
token: Annotated[HTTPAuthorizationCredentials, Depends(auth_scheme)],
|
||||||
|
amount: int,
|
||||||
|
code: str | None = None,
|
||||||
|
):
|
||||||
|
user_id = await auth_service.verify_token(token.credentials)
|
||||||
|
url = await payment_service.request(user_id=user_id, amount=amount, code=code)
|
||||||
|
|
||||||
|
return {"url": url}
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/plan", response_model=Plans)
|
||||||
|
async def get_plans():
|
||||||
|
result = await payment_service.get_plans()
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/history", response_model=Billings)
|
||||||
|
async def history(token: Annotated[HTTPAuthorizationCredentials, Depends(auth_scheme)]):
|
||||||
|
user_id = await auth_service.verify_token(token.credentials)
|
||||||
|
billings = await payment_service.history(user_id=user_id)
|
||||||
|
|
||||||
|
return {"billings": billings}
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/verify")
|
||||||
|
async def verify(Status: str, Authority: str):
|
||||||
|
status, msg, credit = await payment_service.verify(
|
||||||
|
status=Status, authority=Authority
|
||||||
|
)
|
||||||
|
|
||||||
|
return RedirectResponse(
|
||||||
|
url=f"https://web.houshan.ai/purchase?status={status}&msg={msg}&credit={credit}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/bazzar", response_model=Credit)
|
||||||
|
async def bazzar(
|
||||||
|
token: Annotated[HTTPAuthorizationCredentials, Depends(auth_scheme)],
|
||||||
|
order_id: str,
|
||||||
|
purchase_token: str,
|
||||||
|
product_id: str,
|
||||||
|
):
|
||||||
|
user_id = await auth_service.verify_token(token.credentials)
|
||||||
|
|
||||||
|
credit = await payment_service.bazzar(
|
||||||
|
user_id=user_id,
|
||||||
|
order_id=order_id,
|
||||||
|
product_id=product_id,
|
||||||
|
purchase_token=purchase_token,
|
||||||
|
)
|
||||||
|
|
||||||
|
return {"credit": credit}
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/myket", response_model=Credit)
|
||||||
|
async def myket(
|
||||||
|
token: Annotated[HTTPAuthorizationCredentials, Depends(auth_scheme)],
|
||||||
|
order_id: str,
|
||||||
|
purchase_token: str,
|
||||||
|
product_id: str,
|
||||||
|
):
|
||||||
|
user_id = await auth_service.verify_token(token.credentials)
|
||||||
|
|
||||||
|
credit = await payment_service.myket(
|
||||||
|
user_id=user_id,
|
||||||
|
order_id=order_id,
|
||||||
|
product_id=product_id,
|
||||||
|
purchase_token=purchase_token,
|
||||||
|
)
|
||||||
|
|
||||||
|
return {"credit": credit}
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/settlement")
|
||||||
|
async def settlement(
|
||||||
|
token: Annotated[HTTPAuthorizationCredentials, Depends(auth_scheme)],
|
||||||
|
settlement: Settlement,
|
||||||
|
):
|
||||||
|
user_id = await auth_service.verify_token(token.credentials)
|
||||||
|
await payment_service.settlement(user_id=user_id, type=settlement.type)
|
||||||
|
|
||||||
|
return {"msg": MESSAGES["settlement"]["success"]}
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/{id}/{key}")
|
||||||
|
async def stream_file(id: str, key: str):
|
||||||
|
file_size = await storage_service.get_file_size(f"payment/{id}/{key}")
|
||||||
|
headers = {"Content-Length": str(file_size)}
|
||||||
|
|
||||||
|
return StreamingResponse(
|
||||||
|
storage_service.get(f"payment/{id}/{key}"),
|
||||||
|
media_type="application/octet-stream",
|
||||||
|
headers=headers,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
##### V2 Routes #####
|
||||||
|
|
||||||
|
|
||||||
|
router_v2 = APIRouter(
|
||||||
|
prefix="/v2/paymant",
|
||||||
|
tags=["paymant"],
|
||||||
|
responses={404: {"detail": "Not found"}},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router_v2.get("/plan", response_model=PlansV2)
|
||||||
|
async def get_plans_v2():
|
||||||
|
plans = await payment_service.get_plans_v2()
|
||||||
|
|
||||||
|
return {"plans": plans}
|
||||||
|
|
||||||
|
@router.post("/basa/increase-credit")
|
||||||
|
async def increase_basa_credit(
|
||||||
|
request_data: BasaCreditRequest,
|
||||||
|
# x_api_key: str = Header(...) # پیشنهاد: دریافت کلید امنیتی از هدر
|
||||||
|
):
|
||||||
|
# if x_api_key != settings.basa_api_secret:
|
||||||
|
# raise HTTPException(status_code=403, detail="Invalid API Key")
|
||||||
|
|
||||||
|
result = await payment_service.add_basa_credit(
|
||||||
|
user_id=request_data.user_id,
|
||||||
|
amount=request_data.amount
|
||||||
|
)
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
@ -0,0 +1,89 @@
|
||||||
|
from uuid import UUID
|
||||||
|
from fastapi import APIRouter, Depends, Form, File, UploadFile
|
||||||
|
from fastapi.security import HTTPAuthorizationCredentials
|
||||||
|
from typing import Annotated
|
||||||
|
from ..schemas.main import BaseResponse
|
||||||
|
from ..schemas.ticket import Ticket, Tickets
|
||||||
|
from ..dependencies import auth_scheme
|
||||||
|
from ..services import ticket as ticket_service, auth as auth_service
|
||||||
|
|
||||||
|
router = APIRouter(
|
||||||
|
prefix="/ticket",
|
||||||
|
tags=["ticket"],
|
||||||
|
responses={404: {"detail": "Not found"}},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/", response_model=Ticket)
|
||||||
|
async def send(
|
||||||
|
token: Annotated[HTTPAuthorizationCredentials, Depends(auth_scheme)],
|
||||||
|
text: Annotated[str | None, Form()] = None,
|
||||||
|
file: Annotated[UploadFile | None, File()] = None,
|
||||||
|
):
|
||||||
|
user_id = await auth_service.verify_token(token.credentials)
|
||||||
|
ticket = await ticket_service.send(user_id=user_id, text=text, file=file)
|
||||||
|
|
||||||
|
return {"ticket": ticket}
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/", response_model=Tickets)
|
||||||
|
async def get_all(
|
||||||
|
token: Annotated[HTTPAuthorizationCredentials, Depends(auth_scheme)],
|
||||||
|
):
|
||||||
|
user_id = await auth_service.verify_token(token.credentials)
|
||||||
|
tickets = await ticket_service.get_all(user_id)
|
||||||
|
|
||||||
|
return {"tickets": tickets}
|
||||||
|
|
||||||
|
|
||||||
|
@router.delete("/{id}", response_model=BaseResponse)
|
||||||
|
async def delete(
|
||||||
|
token: Annotated[HTTPAuthorizationCredentials, Depends(auth_scheme)], id: int
|
||||||
|
):
|
||||||
|
user_id = await auth_service.verify_token(token.credentials)
|
||||||
|
await ticket_service.delete(user_id, id)
|
||||||
|
|
||||||
|
return BaseResponse(detail="Ticket has been successfully deleted")
|
||||||
|
|
||||||
|
|
||||||
|
##### Admin Routes #####
|
||||||
|
|
||||||
|
admin_router = APIRouter(
|
||||||
|
prefix="/admin/ticket",
|
||||||
|
tags=["admin"],
|
||||||
|
responses={404: {"detail": "Not found"}},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@admin_router.get("/", summary="List of Users Who Sent Tickets")
|
||||||
|
async def get_all_users_admin(
|
||||||
|
token: Annotated[HTTPAuthorizationCredentials, Depends(auth_scheme)], page: int
|
||||||
|
):
|
||||||
|
await auth_service.verify_admin_token(token.credentials)
|
||||||
|
result = await ticket_service.get_all_admin(page)
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
@admin_router.post("/", response_model=Ticket, summary="Send Ticket")
|
||||||
|
async def send_admin(
|
||||||
|
token: Annotated[HTTPAuthorizationCredentials, Depends(auth_scheme)],
|
||||||
|
user_id: Annotated[UUID, Form()],
|
||||||
|
text: Annotated[str | None, Form()] = None,
|
||||||
|
file: Annotated[UploadFile | None, File()] = None,
|
||||||
|
):
|
||||||
|
await auth_service.verify_admin_token(token.credentials)
|
||||||
|
ticket = await ticket_service.sendـadmin(user_id, text, file)
|
||||||
|
|
||||||
|
return {"ticket": ticket}
|
||||||
|
|
||||||
|
|
||||||
|
@admin_router.get("/{id}", summary="Get All Tickets From One User")
|
||||||
|
async def get_all_admin(
|
||||||
|
token: Annotated[HTTPAuthorizationCredentials, Depends(auth_scheme)],
|
||||||
|
id: UUID,
|
||||||
|
):
|
||||||
|
await auth_service.verify_admin_token(token.credentials)
|
||||||
|
result = await ticket_service.get_all(id)
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
@ -0,0 +1,244 @@
|
||||||
|
import tempfile
|
||||||
|
from fastapi import APIRouter, Depends, Form, File, UploadFile
|
||||||
|
from fastapi.responses import StreamingResponse
|
||||||
|
from fastapi.security import HTTPAuthorizationCredentials
|
||||||
|
from typing import Annotated
|
||||||
|
from ..config import settings
|
||||||
|
from ..dependencies import auth_scheme
|
||||||
|
from ..schemas.chatbot import MessageStream
|
||||||
|
from ..services import (
|
||||||
|
tool as tool_service,
|
||||||
|
auth as auth_service,
|
||||||
|
storage as storage_service,
|
||||||
|
)
|
||||||
|
|
||||||
|
router = APIRouter(
|
||||||
|
prefix="/tool",
|
||||||
|
tags=["tool"],
|
||||||
|
responses={404: {"detail": "Not found"}},
|
||||||
|
)
|
||||||
|
|
||||||
|
SUNO_AI = [783, 871, 957, 958, 959, 960, 961, 962, 962, 963, 964, 965]
|
||||||
|
PIXVERSE_AI=[955]
|
||||||
|
PIXVERSE_TXT2IMG_AI=[970]
|
||||||
|
FAL_AI = [116, 123, 124, 414, 882, 883, 884, 885, 886, 887, 888, 891, 892, 893, 894]
|
||||||
|
OPENAI_FM=[1050, 1242, 1243, 1244, 1245, 1246]
|
||||||
|
|
||||||
|
@router.post("/{bot_id}", response_model=MessageStream)
|
||||||
|
async def tool(
|
||||||
|
bot_id: int,
|
||||||
|
token: Annotated[HTTPAuthorizationCredentials, Depends(auth_scheme)],
|
||||||
|
query: Annotated[str | None, Form()] = None,
|
||||||
|
id: Annotated[int | None, Form()] = None,
|
||||||
|
retry: Annotated[bool, Form()] = False,
|
||||||
|
ghost: Annotated[bool, Form()] = False,
|
||||||
|
image: Annotated[UploadFile | None, File()] = None,
|
||||||
|
audio: Annotated[UploadFile | None, File()] = None,
|
||||||
|
doc: Annotated[UploadFile | None, File()] = None,
|
||||||
|
options: Annotated[str | None, Form()] = "Hot Harley Quinn"
|
||||||
|
):
|
||||||
|
# Verify user authentication
|
||||||
|
user_id = await auth_service.verify_token(token.credentials)
|
||||||
|
chat_user_id = settings.ghost_id if ghost else user_id
|
||||||
|
|
||||||
|
# Handle file uploads generically
|
||||||
|
image_url = None
|
||||||
|
if image:
|
||||||
|
image_url = await storage_service.upload(id=chat_user_id, file=image)
|
||||||
|
|
||||||
|
audio_url = None
|
||||||
|
audio_path = None
|
||||||
|
if audio:
|
||||||
|
mime_type = audio.content_type.split("/")[0]
|
||||||
|
extension = audio.filename.split(".")[-1]
|
||||||
|
with tempfile.NamedTemporaryFile(
|
||||||
|
delete=False, suffix=f".{extension}"
|
||||||
|
) as tmp_file:
|
||||||
|
tmp_file.write(await audio.read())
|
||||||
|
audio_path = tmp_file.name
|
||||||
|
with open(audio_path, "rb") as f:
|
||||||
|
audio_url = await storage_service.upload_buffer(
|
||||||
|
user_id=chat_user_id,
|
||||||
|
file=f,
|
||||||
|
mime_type=mime_type,
|
||||||
|
extension=extension,
|
||||||
|
)
|
||||||
|
|
||||||
|
doc_url = None
|
||||||
|
doc_path = None
|
||||||
|
if doc:
|
||||||
|
mime_type = doc.content_type.split("/")[0]
|
||||||
|
extension = doc.filename.split(".")[-1]
|
||||||
|
with tempfile.NamedTemporaryFile(
|
||||||
|
delete=False, suffix=f".{extension}"
|
||||||
|
) as tmp_file:
|
||||||
|
tmp_file.write(await doc.read())
|
||||||
|
doc_path = tmp_file.name
|
||||||
|
with open(doc_path, "rb") as f:
|
||||||
|
doc_url = await storage_service.upload_buffer(
|
||||||
|
user_id=chat_user_id,
|
||||||
|
file=f,
|
||||||
|
mime_type=mime_type,
|
||||||
|
extension=extension,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Speech-to-text
|
||||||
|
if bot_id == 14:
|
||||||
|
return StreamingResponse(
|
||||||
|
tool_service.speech_to_text(
|
||||||
|
id=id,
|
||||||
|
user_id=user_id,
|
||||||
|
bot_id=bot_id,
|
||||||
|
ghost=ghost,
|
||||||
|
audio=[
|
||||||
|
(settings.base_url + audio_url) if audio_url else None,
|
||||||
|
audio_path,
|
||||||
|
],
|
||||||
|
),
|
||||||
|
media_type="application/json",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Image-to-image generation (Clarity Upscaler, Background Remover, Ghibli)
|
||||||
|
elif bot_id in [391, 392, 1320]:
|
||||||
|
return StreamingResponse(
|
||||||
|
tool_service.fal_ai_image_to_image_generation(
|
||||||
|
id=id,
|
||||||
|
user_id=user_id,
|
||||||
|
bot_id=bot_id,
|
||||||
|
ghost=ghost,
|
||||||
|
image=(settings.base_url + image_url) if image_url else None,
|
||||||
|
),
|
||||||
|
media_type="application/json",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Image personalization (Avatar, Gender, Caricature)
|
||||||
|
elif bot_id in [438, 453, 454]:
|
||||||
|
return StreamingResponse(
|
||||||
|
tool_service.fal_ai_image_personalization(
|
||||||
|
id=id,
|
||||||
|
user_id=user_id,
|
||||||
|
bot_id=bot_id,
|
||||||
|
ghost=ghost,
|
||||||
|
image=(settings.base_url + image_url) if image_url else None,
|
||||||
|
),
|
||||||
|
media_type="application/json",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Audio generation (Suno AI)
|
||||||
|
elif bot_id in SUNO_AI:
|
||||||
|
return StreamingResponse(
|
||||||
|
tool_service.suno_ai_audio_generator(
|
||||||
|
id=id,
|
||||||
|
user_id=user_id,
|
||||||
|
bot_id=bot_id,
|
||||||
|
ghost=ghost,
|
||||||
|
query=query,
|
||||||
|
),
|
||||||
|
media_type="application/json",
|
||||||
|
)
|
||||||
|
|
||||||
|
# OpenAI text generation
|
||||||
|
elif bot_id == 11:
|
||||||
|
return StreamingResponse(
|
||||||
|
tool_service.text_to_speech(
|
||||||
|
id=id,
|
||||||
|
query=query,
|
||||||
|
user_id=user_id,
|
||||||
|
bot_id=bot_id,
|
||||||
|
ghost=ghost,
|
||||||
|
),
|
||||||
|
media_type="application/json",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Translator
|
||||||
|
elif bot_id in [12, 494]:
|
||||||
|
return StreamingResponse(
|
||||||
|
tool_service.translator(
|
||||||
|
id=id,
|
||||||
|
query=query,
|
||||||
|
user_id=user_id,
|
||||||
|
bot_id=bot_id,
|
||||||
|
ghost=ghost,
|
||||||
|
),
|
||||||
|
media_type="application/json",
|
||||||
|
)
|
||||||
|
|
||||||
|
# OpenAI.FM TTS
|
||||||
|
elif bot_id in OPENAI_FM:
|
||||||
|
return StreamingResponse(
|
||||||
|
tool_service.tts_openai(
|
||||||
|
id=id,
|
||||||
|
query=query,
|
||||||
|
bot_id=bot_id,
|
||||||
|
user_id=user_id,
|
||||||
|
ghost=ghost,
|
||||||
|
),
|
||||||
|
media_type="application/json",
|
||||||
|
)
|
||||||
|
|
||||||
|
# FAL AI image generation
|
||||||
|
elif bot_id in FAL_AI:
|
||||||
|
return StreamingResponse(
|
||||||
|
tool_service.fal_ai_image_generation(
|
||||||
|
id=id,
|
||||||
|
query=query,
|
||||||
|
bot_id=bot_id,
|
||||||
|
user_id=user_id,
|
||||||
|
ghost=ghost,
|
||||||
|
),
|
||||||
|
media_type="application/json",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Image personalization (Avatar, Gender, Caricature)
|
||||||
|
elif bot_id in PIXVERSE_AI:
|
||||||
|
await image.seek(0)
|
||||||
|
image_bytes = await image.read() if image is not None else None
|
||||||
|
|
||||||
|
return StreamingResponse(
|
||||||
|
tool_service.pixverse_image2video_generator(
|
||||||
|
id=id,
|
||||||
|
image_bytes=image_bytes,
|
||||||
|
user_id=user_id,
|
||||||
|
bot_id=bot_id,
|
||||||
|
ghost=ghost,
|
||||||
|
effect=options,
|
||||||
|
user_image_url=(settings.base_url + image_url) if image_url else None,
|
||||||
|
),
|
||||||
|
media_type="application/json",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Text to image
|
||||||
|
elif bot_id in PIXVERSE_TXT2IMG_AI:
|
||||||
|
return StreamingResponse(
|
||||||
|
tool_service.pixverse_text2video_generator(
|
||||||
|
id=id,
|
||||||
|
user_id=user_id,
|
||||||
|
bot_id=bot_id,
|
||||||
|
ghost=ghost,
|
||||||
|
prompt=query
|
||||||
|
),
|
||||||
|
media_type="application/json",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Catch-all for other bot_ids
|
||||||
|
else:
|
||||||
|
return StreamingResponse(
|
||||||
|
tool_service.others(
|
||||||
|
id=id,
|
||||||
|
query=query,
|
||||||
|
user_id=user_id,
|
||||||
|
bot_id=bot_id,
|
||||||
|
retry=retry,
|
||||||
|
ghost=ghost,
|
||||||
|
image=(settings.base_url + image_url) if image_url else None,
|
||||||
|
audio=[
|
||||||
|
(settings.base_url + audio_url) if audio_url else None,
|
||||||
|
audio_path,
|
||||||
|
],
|
||||||
|
doc=[
|
||||||
|
(settings.base_url + doc_url) if doc_url else None,
|
||||||
|
doc_path,
|
||||||
|
],
|
||||||
|
),
|
||||||
|
media_type="application/json",
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,426 @@
|
||||||
|
from uuid import UUID
|
||||||
|
from datetime import date
|
||||||
|
from typing import Annotated
|
||||||
|
from fastapi import APIRouter, Depends, UploadFile
|
||||||
|
from fastapi.security import HTTPAuthorizationCredentials
|
||||||
|
from fastapi.responses import StreamingResponse
|
||||||
|
from ..dependencies import auth_scheme
|
||||||
|
from ..schemas.token import Token, BaseResponse
|
||||||
|
from ..services import (
|
||||||
|
user as user_service,
|
||||||
|
otp as otp_service,
|
||||||
|
auth as auth_service,
|
||||||
|
storage as storage_service,
|
||||||
|
)
|
||||||
|
from ..schemas.user import (
|
||||||
|
UserBase,
|
||||||
|
UserLoginWithOTP,
|
||||||
|
UserLogin,
|
||||||
|
User,
|
||||||
|
Username,
|
||||||
|
Available,
|
||||||
|
Profile,
|
||||||
|
Password,
|
||||||
|
CardNumber,
|
||||||
|
Code,
|
||||||
|
CoinReport,
|
||||||
|
WeeklyReport,
|
||||||
|
DailyReport,
|
||||||
|
Firebase,
|
||||||
|
IsNew,
|
||||||
|
Users,
|
||||||
|
OneUser,
|
||||||
|
UserLevel,
|
||||||
|
TelegramSyncRequest,
|
||||||
|
TelegramLoginRequest,
|
||||||
|
UserLevel,
|
||||||
|
CreateSubUser,
|
||||||
|
SubUsersList,
|
||||||
|
GroupScore
|
||||||
|
)
|
||||||
|
from ..config import settings
|
||||||
|
|
||||||
|
router = APIRouter(
|
||||||
|
prefix="/user",
|
||||||
|
tags=["user"],
|
||||||
|
responses={404: {"detail": "Not found"}},
|
||||||
|
)
|
||||||
|
|
||||||
|
def check_telegram_secret(secret: str):
|
||||||
|
return secret == settings.internal_telegram_secret
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/otp", response_model=BaseResponse, summary="Send OTP")
|
||||||
|
async def send_otp(user: UserBase):
|
||||||
|
print(f"--- [WARNING] ---")
|
||||||
|
print(f"--- [WARNING] ---")
|
||||||
|
print(f"--- [WARNING] ---")
|
||||||
|
print(f"--- [WARNING] ---")
|
||||||
|
print(f"--- [WARNING] ---")
|
||||||
|
print(f"--- [WARNING] ---")
|
||||||
|
print(f"--- [WARNING] ---")
|
||||||
|
await otp_service.generate(user.mobile_number)
|
||||||
|
|
||||||
|
return BaseResponse(detail="OTP has been successfully sent")
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/telegram-login")
|
||||||
|
async def telegram_sync_password(request: TelegramLoginRequest):
|
||||||
|
if not check_telegram_secret(request.secret):
|
||||||
|
return BaseResponse(detail="Invalid telegram secret")
|
||||||
|
|
||||||
|
result = await user_service.telegram_login(request.telegram_id)
|
||||||
|
return {
|
||||||
|
"access_token": result.get("access_token"),
|
||||||
|
"token_type": "Bearer",
|
||||||
|
"user": result.get("user"),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/telegram-sync")
|
||||||
|
async def telegram_sync_password(request: TelegramSyncRequest):
|
||||||
|
if not check_telegram_secret(request.secret):
|
||||||
|
return BaseResponse(detail="Invalid telegram secret")
|
||||||
|
|
||||||
|
result = await user_service.telegram_sync(request)
|
||||||
|
|
||||||
|
if result:
|
||||||
|
return BaseResponse(detail="Telegram user has been synced")
|
||||||
|
return {
|
||||||
|
"message": "Telegram user has not been synced",
|
||||||
|
"user": result
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/register", response_model=BaseResponse)
|
||||||
|
async def register(user: UserBase):
|
||||||
|
await user_service.register(user.mobile_number)
|
||||||
|
|
||||||
|
return BaseResponse(detail="OTP has been successfully sent")
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/login", response_model=Token, summary="Login With Password")
|
||||||
|
async def login(user: UserLogin):
|
||||||
|
access_token = await user_service.login(user.username, user.password)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"access_token": access_token,
|
||||||
|
"token_type": "Bearer",
|
||||||
|
"user": None
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/login/otp", response_model=Token, summary="Login With OTP")
|
||||||
|
async def login_with_otp(user: UserLoginWithOTP):
|
||||||
|
access_token = await user_service.login_with_otp(user.mobile_number, user.otp)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"access_token": access_token,
|
||||||
|
"token_type": "Bearer",
|
||||||
|
"user": None
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/info", response_model=User)
|
||||||
|
async def get_info(
|
||||||
|
token: Annotated[HTTPAuthorizationCredentials, Depends(auth_scheme)],
|
||||||
|
):
|
||||||
|
user_id = await auth_service.verify_token(token.credentials)
|
||||||
|
user = await user_service.get_userAndVerify(user_id)
|
||||||
|
|
||||||
|
return user
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/username", response_model=Available)
|
||||||
|
async def check_username(
|
||||||
|
token: Annotated[HTTPAuthorizationCredentials, Depends(auth_scheme)], user: Username
|
||||||
|
):
|
||||||
|
await auth_service.verify_token(token.credentials)
|
||||||
|
available = await user_service.check_username(username=user.username)
|
||||||
|
|
||||||
|
return {"available": available}
|
||||||
|
|
||||||
|
|
||||||
|
@router.put("/username", response_model=BaseResponse)
|
||||||
|
async def edit_username(
|
||||||
|
token: Annotated[HTTPAuthorizationCredentials, Depends(auth_scheme)], user: Username
|
||||||
|
):
|
||||||
|
user_id = await auth_service.verify_token(token.credentials)
|
||||||
|
await user_service.edit_username(user_id=user_id, username=user.username)
|
||||||
|
|
||||||
|
return BaseResponse(detail="Username has been successfully edited")
|
||||||
|
|
||||||
|
|
||||||
|
@router.put("/card-number", response_model=BaseResponse)
|
||||||
|
async def edit_card_number(
|
||||||
|
token: Annotated[HTTPAuthorizationCredentials, Depends(auth_scheme)],
|
||||||
|
user: CardNumber,
|
||||||
|
):
|
||||||
|
user_id = await auth_service.verify_token(token.credentials)
|
||||||
|
await user_service.edit_card_number(user_id=user_id, card_number=user.card_number)
|
||||||
|
|
||||||
|
return BaseResponse(detail="Card number has been successfully edited")
|
||||||
|
|
||||||
|
|
||||||
|
@router.put("/firebase-token", response_model=BaseResponse)
|
||||||
|
async def set_firebase_token(
|
||||||
|
token: Annotated[HTTPAuthorizationCredentials, Depends(auth_scheme)],
|
||||||
|
user: Firebase,
|
||||||
|
):
|
||||||
|
user_id = await auth_service.verify_token(token.credentials)
|
||||||
|
await user_service.set_firebase_token(user_id=user_id, token=user.token)
|
||||||
|
|
||||||
|
return BaseResponse(detail="Firebase token has been successfully registered")
|
||||||
|
|
||||||
|
|
||||||
|
@router.put("/profile", response_model=Profile)
|
||||||
|
async def edit_profile(
|
||||||
|
token: Annotated[HTTPAuthorizationCredentials, Depends(auth_scheme)],
|
||||||
|
file: UploadFile,
|
||||||
|
):
|
||||||
|
user_id = await auth_service.verify_token(token.credentials)
|
||||||
|
url = await user_service.edit_profile(user_id=user_id, file=file)
|
||||||
|
|
||||||
|
return {"image": url}
|
||||||
|
|
||||||
|
|
||||||
|
@router.delete("/profile", response_model=BaseResponse)
|
||||||
|
async def delete_profile(
|
||||||
|
token: Annotated[HTTPAuthorizationCredentials, Depends(auth_scheme)],
|
||||||
|
):
|
||||||
|
user_id = await auth_service.verify_token(token.credentials)
|
||||||
|
await user_service.delete_profile(user_id=user_id)
|
||||||
|
|
||||||
|
return BaseResponse(detail="Profile has been successfully deleted")
|
||||||
|
|
||||||
|
|
||||||
|
@router.put("/password", response_model=BaseResponse)
|
||||||
|
async def edit_password(
|
||||||
|
token: Annotated[HTTPAuthorizationCredentials, Depends(auth_scheme)], user: Password
|
||||||
|
):
|
||||||
|
user_id = await auth_service.verify_token(token.credentials)
|
||||||
|
await user_service.edit_password(user_id=user_id, password=user.password)
|
||||||
|
|
||||||
|
return BaseResponse(detail="Password has been successfully edited")
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/code", response_model=BaseResponse)
|
||||||
|
async def code(
|
||||||
|
token: Annotated[HTTPAuthorizationCredentials, Depends(auth_scheme)], user: Code
|
||||||
|
):
|
||||||
|
user_id = await auth_service.verify_token(token.credentials)
|
||||||
|
message = await user_service.register_code(user_id=user_id, code=user.code)
|
||||||
|
|
||||||
|
return BaseResponse(detail=message)
|
||||||
|
|
||||||
|
|
||||||
|
@router.put("/notification", response_model=BaseResponse)
|
||||||
|
async def mark_all_notifications_as_seen(
|
||||||
|
token: Annotated[HTTPAuthorizationCredentials, Depends(auth_scheme)],
|
||||||
|
):
|
||||||
|
user_id = await auth_service.verify_token(token.credentials)
|
||||||
|
await user_service.mark_all_notifications_as_seen(user_id=user_id)
|
||||||
|
|
||||||
|
return BaseResponse(detail="All notifications has been successfully marked as seen")
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/report/coin", response_model=CoinReport)
|
||||||
|
async def coin_usage_report(
|
||||||
|
token: Annotated[HTTPAuthorizationCredentials, Depends(auth_scheme)],
|
||||||
|
start_date: date | None = None,
|
||||||
|
end_date: date | None = None,
|
||||||
|
):
|
||||||
|
user_id = await auth_service.verify_token(token.credentials)
|
||||||
|
report = await user_service.coin_usage_report(
|
||||||
|
user_id=user_id, start_date=start_date, end_date=end_date
|
||||||
|
)
|
||||||
|
|
||||||
|
return {"report": report}
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/report/periodic", response_model=WeeklyReport)
|
||||||
|
async def periodic_usage_report(
|
||||||
|
token: Annotated[HTTPAuthorizationCredentials, Depends(auth_scheme)],
|
||||||
|
start_date: date,
|
||||||
|
end_date: date,
|
||||||
|
):
|
||||||
|
user_id = await auth_service.verify_token(token.credentials)
|
||||||
|
report = await user_service.periodic_usage_report(user_id, start_date, end_date)
|
||||||
|
|
||||||
|
return {"report": report}
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/report/weekly", response_model=WeeklyReport)
|
||||||
|
async def weekly_usage_report(
|
||||||
|
token: Annotated[HTTPAuthorizationCredentials, Depends(auth_scheme)],
|
||||||
|
start_date: date,
|
||||||
|
):
|
||||||
|
user_id = await auth_service.verify_token(token.credentials)
|
||||||
|
report = await user_service.weekly_usage_report(user_id, start_date)
|
||||||
|
|
||||||
|
return {"report": report}
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/report/daily", response_model=DailyReport)
|
||||||
|
async def daily_usage_report(
|
||||||
|
token: Annotated[HTTPAuthorizationCredentials, Depends(auth_scheme)],
|
||||||
|
start_date: date,
|
||||||
|
):
|
||||||
|
user_id = await auth_service.verify_token(token.credentials)
|
||||||
|
report = await user_service.daily_usage_report(user_id, start_date)
|
||||||
|
|
||||||
|
return {"report": report}
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/{id}/{key}")
|
||||||
|
async def stream_file(
|
||||||
|
# token: Annotated[HTTPAuthorizationCredentials, Depends(auth_scheme)],
|
||||||
|
id: UUID,
|
||||||
|
key: str,
|
||||||
|
):
|
||||||
|
# user_id = await auth_service.verify_token(token.credentials)
|
||||||
|
|
||||||
|
# if id != user_id:
|
||||||
|
# raise HTTPException(status_code=403, detail="Forbidden")
|
||||||
|
|
||||||
|
file_size = await storage_service.get_file_size(f"user/{id}/{key}")
|
||||||
|
headers = {"Content-Length": str(file_size)}
|
||||||
|
|
||||||
|
return StreamingResponse(
|
||||||
|
storage_service.get(f"user/{id}/{key}"),
|
||||||
|
media_type="application/octet-stream",
|
||||||
|
headers=headers,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
##### Admin Routes #####
|
||||||
|
|
||||||
|
admin_router = APIRouter(
|
||||||
|
prefix="/admin/user",
|
||||||
|
tags=["admin"],
|
||||||
|
responses={404: {"detail": "Not found"}},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@admin_router.get("/", response_model=Users)
|
||||||
|
async def get_users(
|
||||||
|
token: Annotated[HTTPAuthorizationCredentials, Depends(auth_scheme)],
|
||||||
|
page: int = 1,
|
||||||
|
q: str | None = None,
|
||||||
|
):
|
||||||
|
await auth_service.verify_admin_token(token.credentials)
|
||||||
|
result = await user_service.get_users(page, q)
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
@admin_router.post("/login", response_model=Token, summary="Login With Password")
|
||||||
|
async def admin_login(user: UserLogin):
|
||||||
|
access_token = await user_service.admin_login(user.username, user.password)
|
||||||
|
user = await user_service.get_admin_user(user.username)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"access_token": access_token,
|
||||||
|
"token_type": "Bearer",
|
||||||
|
"user": user,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@admin_router.post("/groupscore", response_model=BaseResponse, summary="Add Group Score to All Users")
|
||||||
|
async def add_group_score(
|
||||||
|
data: GroupScore,
|
||||||
|
token: Annotated[HTTPAuthorizationCredentials, Depends(auth_scheme)],
|
||||||
|
):
|
||||||
|
await auth_service.verify_admin_token(token.credentials)
|
||||||
|
await user_service.add_group_score(data.coins,data.price)
|
||||||
|
|
||||||
|
return BaseResponse(detail="امتیاز به تمام کاربران با موفقیت اضافه شد")
|
||||||
|
|
||||||
|
|
||||||
|
@admin_router.get("/{id}", response_model=OneUser)
|
||||||
|
async def get_user(
|
||||||
|
token: Annotated[HTTPAuthorizationCredentials, Depends(auth_scheme)], id: str
|
||||||
|
):
|
||||||
|
await auth_service.verify_admin_token(token.credentials)
|
||||||
|
user = await user_service.admin_get_user(id)
|
||||||
|
|
||||||
|
return {"user": user}
|
||||||
|
|
||||||
|
|
||||||
|
##### V2 Routes #####
|
||||||
|
|
||||||
|
router_v2 = APIRouter(
|
||||||
|
prefix="/v2/user",
|
||||||
|
tags=["user"],
|
||||||
|
responses={404: {"detail": "Not found"}},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@admin_router.put("/{user_id}/level", response_model=BaseResponse, summary="Update User Level")
|
||||||
|
async def update_level(
|
||||||
|
user_id: UUID, # <--- دریافت آیدی کاربر هدف از URL
|
||||||
|
data: UserLevel,
|
||||||
|
token: Annotated[HTTPAuthorizationCredentials, Depends(auth_scheme)],
|
||||||
|
):
|
||||||
|
await auth_service.verify_admin_token(token.credentials)
|
||||||
|
|
||||||
|
await user_service.edit_level(user_id=user_id, level=data.level)
|
||||||
|
|
||||||
|
return BaseResponse(detail="User level has been successfully updated")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@router_v2.post("/otp", response_model=IsNew, summary="Send OTP V2")
|
||||||
|
async def send_otp_v2(user: UserBase):
|
||||||
|
is_new = await otp_service.generate(user.mobile_number)
|
||||||
|
|
||||||
|
return {"is_new": False}
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/sub-user", response_model=BaseResponse, summary="Create Sub User (Level 2)")
|
||||||
|
async def add_sub_user(
|
||||||
|
token: Annotated[HTTPAuthorizationCredentials, Depends(auth_scheme)],
|
||||||
|
data: CreateSubUser
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
کاربر اصلی (سطح ۱) میتواند با این متد یک کاربر فرعی (سطح ۲) ایجاد کند.
|
||||||
|
"""
|
||||||
|
user_id = await auth_service.verify_token(token.credentials)
|
||||||
|
|
||||||
|
await user_service.create_sub_user(
|
||||||
|
parent_id=user_id,
|
||||||
|
mobile_number=data.mobile_number,
|
||||||
|
level = data.level
|
||||||
|
)
|
||||||
|
|
||||||
|
return BaseResponse(detail=" زیر مجموعه شما با موفقیت ایجاد شد")
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/sub-user", response_model=SubUsersList, summary="Get My Sub Users")
|
||||||
|
async def get_sub_users(
|
||||||
|
token: Annotated[HTTPAuthorizationCredentials, Depends(auth_scheme)],
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
مشاهده لیست کاربران فرعی زیرمجموعه.
|
||||||
|
|
||||||
|
"""
|
||||||
|
user_id = await auth_service.verify_token(token.credentials)
|
||||||
|
|
||||||
|
result = await user_service.get_my_sub_users(user_id=user_id)
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
@router.delete("/sub-user/{sub_user_id}", response_model=BaseResponse, summary="Delete Sub User")
|
||||||
|
async def remove_sub_user(
|
||||||
|
sub_user_id: UUID,
|
||||||
|
token: Annotated[HTTPAuthorizationCredentials, Depends(auth_scheme)],
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
حذف یک کاربر زیرمجموعه توسط والد.
|
||||||
|
"""
|
||||||
|
user_id = await auth_service.verify_token(token.credentials)
|
||||||
|
|
||||||
|
await user_service.delete_sub_user(parent_id=user_id, sub_user_id=sub_user_id)
|
||||||
|
|
||||||
|
return BaseResponse(detail="زیرمجموعه با موفقیت حذف شد")
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
from pydantic import BaseModel
|
||||||
|
from enum import Enum
|
||||||
|
|
||||||
|
class AdEnum(str, Enum):
|
||||||
|
REWARDED_VIDEO = "rewarded_video"
|
||||||
|
Interstitial = "interstitial"
|
||||||
|
Native = "native"
|
||||||
|
Standard = "standard"
|
||||||
|
|
||||||
|
class Advertisement(BaseModel):
|
||||||
|
ad_type: AdEnum
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
from pydantic import BaseModel
|
||||||
|
from .bot import BotInfo
|
||||||
|
|
||||||
|
|
||||||
|
class Banner(BaseModel):
|
||||||
|
image: str
|
||||||
|
link: str | None
|
||||||
|
title: str | None
|
||||||
|
description: str | None
|
||||||
|
bot: BotInfo | None
|
||||||
|
|
||||||
|
|
||||||
|
class Banners(BaseModel):
|
||||||
|
banners: list[Banner]
|
||||||
|
|
@ -0,0 +1,197 @@
|
||||||
|
from datetime import datetime
|
||||||
|
from pydantic import BaseModel, validator
|
||||||
|
from ..models import BotStatus, BotType
|
||||||
|
|
||||||
|
|
||||||
|
class BotUser(BaseModel):
|
||||||
|
name: str | None
|
||||||
|
username: str | None
|
||||||
|
|
||||||
|
|
||||||
|
class BotCategory(BaseModel):
|
||||||
|
id: int
|
||||||
|
name: str
|
||||||
|
|
||||||
|
|
||||||
|
class BaseBot(BaseModel):
|
||||||
|
id: int
|
||||||
|
image: str | None
|
||||||
|
image_2: str | None
|
||||||
|
model: str
|
||||||
|
name: str | None
|
||||||
|
description: str | None
|
||||||
|
|
||||||
|
|
||||||
|
class BotLevelUpdate(BaseModel):
|
||||||
|
allowed_levels: list[int] | None = []
|
||||||
|
|
||||||
|
|
||||||
|
class BotInfo(BaseBot):
|
||||||
|
cost: int
|
||||||
|
limit: int
|
||||||
|
tool: bool
|
||||||
|
public: bool
|
||||||
|
attachment: int
|
||||||
|
deleted: bool
|
||||||
|
web_search: bool
|
||||||
|
marked: bool | None = None
|
||||||
|
attachment_type: list[str] | None = None
|
||||||
|
type: BotType
|
||||||
|
category: BotCategory | None = None
|
||||||
|
messages: int = 0
|
||||||
|
allowed_levels: list[int] | None = []
|
||||||
|
|
||||||
|
@validator("allowed_levels", pre=True)
|
||||||
|
def ensure_list_levels(cls, v):
|
||||||
|
if v is None:
|
||||||
|
return []
|
||||||
|
return v
|
||||||
|
|
||||||
|
|
||||||
|
class MyBotPreview(BaseBot):
|
||||||
|
score: float | None
|
||||||
|
comments: int
|
||||||
|
status: BotStatus
|
||||||
|
|
||||||
|
@validator("score")
|
||||||
|
def round_score(cls, v):
|
||||||
|
if v is not None:
|
||||||
|
return round(v, 2)
|
||||||
|
return v
|
||||||
|
|
||||||
|
|
||||||
|
class MyBotInfo(BaseBot):
|
||||||
|
prompt: str
|
||||||
|
description: str
|
||||||
|
public: bool
|
||||||
|
docs: list[str] | None
|
||||||
|
links: list[str] | None
|
||||||
|
# تغییر: اجازه دادن به مقدار None برای جلوگیری از خطا
|
||||||
|
category: BotCategory | None
|
||||||
|
|
||||||
|
|
||||||
|
class MyBots(BaseModel):
|
||||||
|
bots: list[MyBotPreview]
|
||||||
|
|
||||||
|
|
||||||
|
class MyBot(BaseModel):
|
||||||
|
bot: MyBotInfo
|
||||||
|
|
||||||
|
|
||||||
|
class Bots(BaseModel):
|
||||||
|
bots: list[BotInfo]
|
||||||
|
|
||||||
|
|
||||||
|
class Mark(BaseModel):
|
||||||
|
marked: bool
|
||||||
|
|
||||||
|
|
||||||
|
class Category(BaseModel):
|
||||||
|
category_name: str
|
||||||
|
bots: list[BotInfo]
|
||||||
|
|
||||||
|
|
||||||
|
class Categories(BaseModel):
|
||||||
|
categories: list[Category]
|
||||||
|
|
||||||
|
|
||||||
|
class CommentUser(BaseModel):
|
||||||
|
username: str | None
|
||||||
|
image: str | None
|
||||||
|
|
||||||
|
|
||||||
|
class BaseComment(BaseModel):
|
||||||
|
text: str
|
||||||
|
score: float
|
||||||
|
|
||||||
|
@validator("score")
|
||||||
|
def round_score(cls, v):
|
||||||
|
if v is not None:
|
||||||
|
return round(v, 2)
|
||||||
|
return v
|
||||||
|
|
||||||
|
|
||||||
|
class Comment(BaseComment):
|
||||||
|
created_at: datetime
|
||||||
|
user: CommentUser
|
||||||
|
|
||||||
|
|
||||||
|
class Comments(BaseModel):
|
||||||
|
comments: list[Comment]
|
||||||
|
page: int
|
||||||
|
total_count: int
|
||||||
|
last_page: int
|
||||||
|
|
||||||
|
|
||||||
|
class Bot(BotInfo):
|
||||||
|
score: float | None
|
||||||
|
comments: int
|
||||||
|
users: int
|
||||||
|
messages: int
|
||||||
|
created_at: datetime
|
||||||
|
user: BotUser | None
|
||||||
|
category: BotCategory | None
|
||||||
|
user_comment: BaseComment | None
|
||||||
|
|
||||||
|
@validator("score")
|
||||||
|
def round_score(cls, v):
|
||||||
|
if v is not None:
|
||||||
|
return round(v, 2)
|
||||||
|
return v
|
||||||
|
|
||||||
|
|
||||||
|
class OneBot(BaseModel):
|
||||||
|
bot: Bot
|
||||||
|
|
||||||
|
|
||||||
|
class Assistant(BaseModel):
|
||||||
|
id: int
|
||||||
|
name: str | None
|
||||||
|
description: str | None
|
||||||
|
image: str | None
|
||||||
|
created_at: datetime
|
||||||
|
# تغییر: اختیاری کردن دسته بندی
|
||||||
|
category: BotCategory | None
|
||||||
|
status: BotStatus
|
||||||
|
|
||||||
|
|
||||||
|
class Assistants(BaseModel):
|
||||||
|
assistants: list[Assistant]
|
||||||
|
page: int
|
||||||
|
total_count: int
|
||||||
|
last_page: int
|
||||||
|
|
||||||
|
|
||||||
|
class AssistantDetail(Assistant):
|
||||||
|
cost: int
|
||||||
|
prompt: str | None
|
||||||
|
public: bool
|
||||||
|
links: list[str] | None
|
||||||
|
docs: list[str] | None
|
||||||
|
# تغییر: رفع خطای user is None
|
||||||
|
user: BotUser | None
|
||||||
|
allowed_levels: list[int] | None = None
|
||||||
|
|
||||||
|
@validator("allowed_levels", pre=True)
|
||||||
|
def ensure_list_levels(cls, v):
|
||||||
|
if v is None:
|
||||||
|
return []
|
||||||
|
return v
|
||||||
|
|
||||||
|
|
||||||
|
class OneAssistant(BaseModel):
|
||||||
|
assistant: AssistantDetail
|
||||||
|
|
||||||
|
|
||||||
|
class Confirm(BaseModel):
|
||||||
|
cost: int
|
||||||
|
message: str
|
||||||
|
allowed_levels: list[int] | None = [1]
|
||||||
|
|
||||||
|
|
||||||
|
class Reject(BaseModel):
|
||||||
|
message: str
|
||||||
|
|
||||||
|
|
||||||
|
class Image(BaseModel):
|
||||||
|
url: str
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
from pydantic import BaseModel
|
||||||
|
from .bot import BotInfo
|
||||||
|
|
||||||
|
|
||||||
|
class Category(BaseModel):
|
||||||
|
id: int
|
||||||
|
name: str
|
||||||
|
icon: str | None
|
||||||
|
image: str | None
|
||||||
|
|
||||||
|
|
||||||
|
class ToolCategory(Category):
|
||||||
|
icon: str | None
|
||||||
|
image: str | None
|
||||||
|
description: str | None
|
||||||
|
bots: list[BotInfo]
|
||||||
|
|
||||||
|
|
||||||
|
class Categories(BaseModel):
|
||||||
|
categories: list[Category]
|
||||||
|
|
||||||
|
|
||||||
|
class ToolCategories(BaseModel):
|
||||||
|
categories: list[ToolCategory]
|
||||||
|
|
@ -0,0 +1,80 @@
|
||||||
|
from pydantic import BaseModel
|
||||||
|
from typing import List
|
||||||
|
from datetime import datetime
|
||||||
|
from .bot import BotInfo
|
||||||
|
|
||||||
|
|
||||||
|
class Url(BaseModel):
|
||||||
|
url: str
|
||||||
|
query: str | None = None
|
||||||
|
attachment: bool | None = None
|
||||||
|
|
||||||
|
|
||||||
|
class Content(BaseModel):
|
||||||
|
type: str
|
||||||
|
text: str | None = None
|
||||||
|
image_url: Url | None = None
|
||||||
|
audio_url: Url | None = None
|
||||||
|
pdf_url: Url | None = None
|
||||||
|
video_url: Url | None = None
|
||||||
|
|
||||||
|
|
||||||
|
class BaseMessage(BaseModel):
|
||||||
|
id: str
|
||||||
|
content: str | List[Content]
|
||||||
|
|
||||||
|
|
||||||
|
class Message(BaseMessage):
|
||||||
|
role: str
|
||||||
|
like: bool | None
|
||||||
|
created_at: datetime | None
|
||||||
|
|
||||||
|
|
||||||
|
class ChatMessage(BaseModel):
|
||||||
|
id: int | None = None
|
||||||
|
model: str
|
||||||
|
query: str
|
||||||
|
bot_id: int
|
||||||
|
retry: bool = False
|
||||||
|
|
||||||
|
|
||||||
|
class Chat(BaseModel):
|
||||||
|
id: int
|
||||||
|
title: str
|
||||||
|
created_at: datetime
|
||||||
|
bot: BotInfo
|
||||||
|
|
||||||
|
|
||||||
|
class ChatHistory(Chat):
|
||||||
|
messages: List[Message]
|
||||||
|
|
||||||
|
|
||||||
|
class Chats(BaseModel):
|
||||||
|
chats: List[Chat]
|
||||||
|
page: int
|
||||||
|
total_count: int
|
||||||
|
last_page: int
|
||||||
|
|
||||||
|
|
||||||
|
class Like(BaseModel):
|
||||||
|
like: bool | None
|
||||||
|
|
||||||
|
|
||||||
|
class Title(BaseModel):
|
||||||
|
title: str
|
||||||
|
|
||||||
|
|
||||||
|
class Archive(BaseModel):
|
||||||
|
archive: bool = False
|
||||||
|
|
||||||
|
|
||||||
|
class RelatedQuestions(BaseModel):
|
||||||
|
questions: List[str]
|
||||||
|
|
||||||
|
|
||||||
|
class MessageStream(BaseModel):
|
||||||
|
chat_id: str
|
||||||
|
chat_title: str
|
||||||
|
content: str
|
||||||
|
ai_message_id: str
|
||||||
|
human_message_id: str
|
||||||
|
|
@ -0,0 +1,56 @@
|
||||||
|
from uuid import UUID
|
||||||
|
from pydantic import BaseModel
|
||||||
|
from datetime import datetime
|
||||||
|
from typing import Literal
|
||||||
|
|
||||||
|
|
||||||
|
class CommentUser(BaseModel):
|
||||||
|
id: UUID
|
||||||
|
name: str | None
|
||||||
|
image: str | None
|
||||||
|
username: str | None
|
||||||
|
|
||||||
|
|
||||||
|
class CommentBase(BaseModel):
|
||||||
|
id: int
|
||||||
|
text: str
|
||||||
|
image: str | None
|
||||||
|
created_at: datetime
|
||||||
|
|
||||||
|
|
||||||
|
class NewComment(BaseModel):
|
||||||
|
comment: CommentBase
|
||||||
|
|
||||||
|
|
||||||
|
class Comment(CommentBase):
|
||||||
|
replies: int
|
||||||
|
likes: int
|
||||||
|
dislikes: int
|
||||||
|
user_feedback: int
|
||||||
|
user: CommentUser
|
||||||
|
|
||||||
|
|
||||||
|
class Reply(CommentBase):
|
||||||
|
likes: int
|
||||||
|
dislikes: int
|
||||||
|
user_feedback: int
|
||||||
|
user: CommentUser
|
||||||
|
replied_user: CommentUser
|
||||||
|
|
||||||
|
|
||||||
|
class Comments(BaseModel):
|
||||||
|
comments: list[Comment]
|
||||||
|
page: int
|
||||||
|
total_count: int
|
||||||
|
last_page: int
|
||||||
|
|
||||||
|
|
||||||
|
class Replies(BaseModel):
|
||||||
|
replies: list[Reply]
|
||||||
|
page: int
|
||||||
|
total_count: int
|
||||||
|
last_page: int
|
||||||
|
|
||||||
|
|
||||||
|
class Feedback(BaseModel):
|
||||||
|
status: Literal[-1, 0, 1]
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
|
||||||
|
class Discount(BaseModel):
|
||||||
|
percent: int | None
|
||||||
|
value: int | None
|
||||||
|
maxValue: int | None
|
||||||
|
bazzar_token: str | None
|
||||||
|
|
||||||
|
|
||||||
|
class Code(BaseModel):
|
||||||
|
code: str
|
||||||
|
product_id: str
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue