firt commit

This commit is contained in:
vahidrezvani 2026-02-21 09:48:28 +03:30
commit b8ab8b4b93
166 changed files with 11222 additions and 0 deletions

29
.api.env Normal file
View File

@ -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

3
.dockerignore Normal file
View File

@ -0,0 +1,3 @@
.venv
.env
db-backup

17
.env Normal file
View File

@ -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

18
.gitignore vendored Normal file
View File

@ -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

44
Dockerfile Normal file
View File

@ -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"]

83
Makefile Normal file
View File

@ -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

20
backup.sh Normal file
View File

@ -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

137
docker-compose.yml Normal file
View File

@ -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

23
frontend/Dockerfile Normal file
View File

@ -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;"]

29
frontend/nginx.conf Normal file
View File

@ -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;
}

View File

@ -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 """
"""

View File

@ -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";"""

View File

@ -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";"""

View File

@ -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;"""

View File

@ -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";"""

View File

@ -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";"""

View File

@ -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";"""

View File

@ -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);"""

View File

@ -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";"""

View File

@ -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";"""

View File

@ -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";"""

View File

@ -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";"""

View File

@ -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";"""

View File

@ -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";"""

View File

@ -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";"""

View File

@ -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";"""

View File

@ -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";"""

View File

@ -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";"""

View File

@ -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";"""

View File

@ -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";"""

View File

@ -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";"""

View File

@ -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";"""

View File

@ -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;"""

View File

@ -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";"""

View File

@ -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";"""

View File

@ -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";"""

View File

@ -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";"""

View File

@ -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";"""

View File

@ -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;"""

View File

@ -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";"""

View File

@ -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;"""

View File

@ -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";"""

View File

@ -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";"""

View File

@ -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";"""

View File

@ -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";"""

93
proxy/nginx.conf Normal file
View File

@ -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;
}
}

33
proxy/nginx.conf.save Normal file
View File

@ -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;
}
}

4
pyproject.toml Normal file
View File

@ -0,0 +1,4 @@
[tool.aerich]
tortoise_orm = "settings.TORTOISE_ORM"
location = "./migrations"
src_folder = "./src"

218
requirements.txt Normal file
View File

@ -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
src/__init__.py Normal file
View File

38
src/config.py Normal file
View File

@ -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()

7
src/dependencies.py Normal file
View File

@ -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"])

252
src/main.py Normal file
View File

@ -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

46
src/messages.py Normal file
View File

@ -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": "لطفا قبل از ثبت نظر، اقدام به استفاده از بات نمایید"},
}

22
src/models/__init__.py Normal file
View File

@ -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

View File

@ -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"

View File

@ -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"

16
src/models/banner.py Normal file
View File

@ -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"

39
src/models/billing.py Normal file
View File

@ -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"

62
src/models/bot.py Normal file
View File

@ -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"

26
src/models/category.py Normal file
View File

@ -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"

20
src/models/chatbot.py Normal file
View File

@ -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"

13
src/models/code.py Normal file
View File

@ -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"

32
src/models/comment.py Normal file
View File

@ -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"

15
src/models/discount.py Normal file
View File

@ -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"

21
src/models/event.py Normal file
View File

@ -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"

24
src/models/feedback.py Normal file
View File

@ -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")

17
src/models/mark.py Normal file
View File

@ -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")

20
src/models/message.py Normal file
View File

@ -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"

View File

@ -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"

17
src/models/plan.py Normal file
View File

@ -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"

18
src/models/rate.py Normal file
View File

@ -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"

22
src/models/settlement.py Normal file
View File

@ -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"

19
src/models/suno_task.py Normal file
View File

@ -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"

22
src/models/ticket.py Normal file
View File

@ -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"

5
src/models/timestamp.py Normal file
View File

@ -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)

51
src/models/user.py Normal file
View File

@ -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"

17
src/models/winner.py Normal file
View File

@ -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"

43
src/routes/__init__.py Normal file
View File

@ -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)

View File

@ -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

View File

@ -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

30
src/routes/banner.py Normal file
View File

@ -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,
)

376
src/routes/bot.py Normal file
View File

@ -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="بات با موفقیت حذف شد")

81
src/routes/category.py Normal file
View File

@ -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}

371
src/routes/chatbot.py Normal file
View File

@ -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")

30
src/routes/discount.py Normal file
View File

@ -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,
}

19
src/routes/event.py Normal file
View File

@ -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}

87
src/routes/forum.py Normal file
View File

@ -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")

150
src/routes/payment.py Normal file
View File

@ -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

89
src/routes/ticket.py Normal file
View File

@ -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

244
src/routes/tool.py Normal file
View File

@ -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",
)

426
src/routes/user.py Normal file
View File

@ -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
src/schemas/__init__.py Normal file
View File

View File

@ -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

14
src/schemas/banner.py Normal file
View File

@ -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]

197
src/schemas/bot.py Normal file
View File

@ -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

24
src/schemas/category.py Normal file
View File

@ -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]

80
src/schemas/chatbot.py Normal file
View File

@ -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

56
src/schemas/comment.py Normal file
View File

@ -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]

13
src/schemas/discount.py Normal file
View File

@ -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