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