Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c65b608f0b | ||
|
|
cff8368b7c | ||
|
|
2f367bbd6b | ||
|
|
c3d6324ec6 | ||
|
|
4800a2da80 | ||
|
|
70a0cd44a1 | ||
|
|
545c9c7d07 | ||
|
|
cd60f496fa | ||
|
|
f883065bb9 | ||
|
|
b71bd71d8d | ||
|
|
0524d24b13 |
@@ -50,6 +50,7 @@ FREEKASSA_MERCHANT_ID=your_shop_id #
|
|||||||
FREEKASSA_API_KEY=your_api_key # API key for REST requests
|
FREEKASSA_API_KEY=your_api_key # API key for REST requests
|
||||||
FREEKASSA_SECOND_SECRET=your_second_secret # Secret word #2 (used to verify notifications)
|
FREEKASSA_SECOND_SECRET=your_second_secret # Secret word #2 (used to verify notifications)
|
||||||
FREEKASSA_PAYMENT_IP= # Public IP address reported to FreeKassa
|
FREEKASSA_PAYMENT_IP= # Public IP address reported to FreeKassa
|
||||||
|
FREEKASSA_PAYMENT_METHOD_ID=44 # Payment method ID, you can get it from https://merchant.freekassa.net/settings/currencies
|
||||||
|
|
||||||
# CryptoBot Payment Gateway Configuration
|
# CryptoBot Payment Gateway Configuration
|
||||||
CRYPTOPAY_TOKEN= # API token for CryptoPay
|
CRYPTOPAY_TOKEN= # API token for CryptoPay
|
||||||
|
|||||||
@@ -9,8 +9,9 @@ on:
|
|||||||
- dev
|
- dev
|
||||||
|
|
||||||
env:
|
env:
|
||||||
REGISTRY: ghcr.io
|
GHCR_REGISTRY: ghcr.io
|
||||||
IMAGE_NAME: ${{ github.repository }}
|
IMAGE_NAME: ${{ github.repository }}
|
||||||
|
DOCKERHUB_IMAGE: docker.io/${{ secrets.DOCKERHUB_USERNAME }}/${{ github.event.repository.name }}
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
@@ -26,18 +27,26 @@ jobs:
|
|||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v3
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
- name: Log in to Container Registry
|
- name: Log in to GitHub Container Registry
|
||||||
uses: docker/login-action@v3
|
uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
registry: ${{ env.REGISTRY }}
|
registry: ${{ env.GHCR_REGISTRY }}
|
||||||
username: ${{ github.actor }}
|
username: ${{ github.actor }}
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Log in to Docker Hub
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Extract metadata (tags, labels) for Docker
|
- name: Extract metadata (tags, labels) for Docker
|
||||||
id: meta
|
id: meta
|
||||||
uses: docker/metadata-action@v5
|
uses: docker/metadata-action@v5
|
||||||
with:
|
with:
|
||||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
images: |
|
||||||
|
${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||||
|
${{ env.DOCKERHUB_IMAGE }}
|
||||||
tags: |
|
tags: |
|
||||||
type=ref,event=branch
|
type=ref,event=branch
|
||||||
type=ref,event=pr
|
type=ref,event=pr
|
||||||
|
|||||||
@@ -9,6 +9,10 @@ on:
|
|||||||
paths-ignore:
|
paths-ignore:
|
||||||
- 'README.md'
|
- 'README.md'
|
||||||
|
|
||||||
|
env:
|
||||||
|
GHCR_IMAGE: ghcr.io/${{ github.repository }}
|
||||||
|
DOCKERHUB_IMAGE: docker.io/${{ secrets.DOCKERHUB_USERNAME }}/${{ github.event.repository.name }}
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build-and-push:
|
build-and-push:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@@ -27,6 +31,12 @@ jobs:
|
|||||||
username: ${{ github.actor }}
|
username: ${{ github.actor }}
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Log in to Docker Hub
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Set up QEMU (для эмуляции arm64 на x86)
|
- name: Set up QEMU (для эмуляции arm64 на x86)
|
||||||
uses: docker/setup-qemu-action@v3
|
uses: docker/setup-qemu-action@v3
|
||||||
|
|
||||||
@@ -40,5 +50,7 @@ jobs:
|
|||||||
platforms: linux/amd64,linux/arm64
|
platforms: linux/amd64,linux/arm64
|
||||||
push: true
|
push: true
|
||||||
tags: |
|
tags: |
|
||||||
ghcr.io/${{ github.repository }}:latest
|
${{ env.GHCR_IMAGE }}:latest
|
||||||
ghcr.io/${{ github.repository }}:${{ github.ref_name }}
|
${{ env.GHCR_IMAGE }}:${{ github.ref_name }}
|
||||||
|
${{ env.DOCKERHUB_IMAGE }}:latest
|
||||||
|
${{ env.DOCKERHUB_IMAGE }}:${{ github.ref_name }}
|
||||||
|
|||||||
@@ -91,6 +91,7 @@
|
|||||||
| `FREEKASSA_SECOND_SECRET` | Секретное слово №2 — используется для проверки уведомлений от FreeKassa. |
|
| `FREEKASSA_SECOND_SECRET` | Секретное слово №2 — используется для проверки уведомлений от FreeKassa. |
|
||||||
| `FREEKASSA_PAYMENT_URL` | (Опционально, legacy SCI) Базовый URL платёжной формы FreeKassa. По умолчанию `https://pay.freekassa.ru/`. |
|
| `FREEKASSA_PAYMENT_URL` | (Опционально, legacy SCI) Базовый URL платёжной формы FreeKassa. По умолчанию `https://pay.freekassa.ru/`. |
|
||||||
| `FREEKASSA_PAYMENT_IP` | Внешний IP вашего сервера, который будет передаваться в запрос оплаты. |
|
| `FREEKASSA_PAYMENT_IP` | Внешний IP вашего сервера, который будет передаваться в запрос оплаты. |
|
||||||
|
| `FREEKASSA_PAYMENT_METHOD_ID` | ID метода оплаты через магазин FreeKassa. По умолчанию `44`. |
|
||||||
| `STARS_ENABLED` | Включить/выключить Telegram Stars (`true`/`false`). |
|
| `STARS_ENABLED` | Включить/выключить Telegram Stars (`true`/`false`). |
|
||||||
| `TRIBUTE_ENABLED`| Включить/выключить Tribute (`true`/`false`). |
|
| `TRIBUTE_ENABLED`| Включить/выключить Tribute (`true`/`false`). |
|
||||||
</details>
|
</details>
|
||||||
@@ -163,6 +164,8 @@
|
|||||||
|
|
||||||
Файлы `Dockerfile` и `docker-compose.yml` уже настроены для сборки и запуска проекта. `docker-compose.yml` использует готовый образ с GitHub Container Registry, но вы можете раскомментировать `build: .` для локальной сборки.
|
Файлы `Dockerfile` и `docker-compose.yml` уже настроены для сборки и запуска проекта. `docker-compose.yml` использует готовый образ с GitHub Container Registry, но вы можете раскомментировать `build: .` для локальной сборки.
|
||||||
|
|
||||||
|
Для автоматической публикации образов настроены GitHub Actions (`.github/workflows`). По умолчанию образы пушатся в GitHub Container Registry и Docker Hub. Добавьте в Secrets репозитория значения `DOCKERHUB_USERNAME` и `DOCKERHUB_TOKEN` (персональный access token или пароль для Docker Hub), чтобы загрузка в Docker Hub работала корректно.
|
||||||
|
|
||||||
## 📁 Структура проекта
|
## 📁 Структура проекта
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -192,6 +192,20 @@ async def format_user_card(user: User, session: AsyncSession,
|
|||||||
trial_status = _("admin_user_trial_used", default="Использовал") if had_subscriptions else _("admin_user_trial_not_used", default="Не использовал")
|
trial_status = _("admin_user_trial_used", default="Использовал") if had_subscriptions else _("admin_user_trial_not_used", default="Не использовал")
|
||||||
card_parts.append(f"{_('admin_user_trial_label', default='🏡 <b>Триал:</b>')} {hcode(trial_status)}")
|
card_parts.append(f"{_('admin_user_trial_label', default='🏡 <b>Триал:</b>')} {hcode(trial_status)}")
|
||||||
|
|
||||||
|
# Financial analytics (admin-only)
|
||||||
|
try:
|
||||||
|
from db.dal import payment_dal
|
||||||
|
|
||||||
|
# Total amount paid by this user
|
||||||
|
total_paid = await payment_dal.get_user_total_paid(session, user.user_id)
|
||||||
|
card_parts.append(f"{_('admin_user_total_paid_label', default='💰 <b>Всего оплачено:</b>')} {hcode(f'{total_paid:.2f} RUB')}")
|
||||||
|
|
||||||
|
# Total revenue from referrals
|
||||||
|
referral_revenue = await payment_dal.get_referral_revenue(session, user.user_id)
|
||||||
|
card_parts.append(f"{_('admin_user_referral_revenue_label', default='💸 <b>Доход по рефералам:</b>')} {hcode(f'{referral_revenue:.2f} RUB')}")
|
||||||
|
except Exception as e_fin:
|
||||||
|
logging.error(f"Failed to build financial analytics for admin card {user.user_id}: {e_fin}")
|
||||||
|
|
||||||
# Referral stats
|
# Referral stats
|
||||||
if referral_service is not None:
|
if referral_service is not None:
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -881,10 +881,10 @@ async def pay_fk_callback_handler(
|
|||||||
months=months,
|
months=months,
|
||||||
amount=price_rub,
|
amount=price_rub,
|
||||||
currency=freekassa_service.default_currency,
|
currency=freekassa_service.default_currency,
|
||||||
method_code=44,
|
payment_method_id=freekassa_service.payment_method_id,
|
||||||
ip_address=freekassa_service.server_ip,
|
ip_address=freekassa_service.server_ip,
|
||||||
extra_params={
|
extra_params={
|
||||||
"us_method": 44,
|
"us_method": freekassa_service.payment_method_id,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -40,6 +40,14 @@ class ChannelSubscriptionMiddleware(BaseMiddleware):
|
|||||||
if not event_user or event_user.id in self.settings.ADMIN_IDS:
|
if not event_user or event_user.id in self.settings.ADMIN_IDS:
|
||||||
return await handler(event, data)
|
return await handler(event, data)
|
||||||
|
|
||||||
|
callback_query = event.callback_query
|
||||||
|
if (
|
||||||
|
callback_query
|
||||||
|
and callback_query.data
|
||||||
|
and callback_query.data == "channel_subscription:verify"
|
||||||
|
):
|
||||||
|
return await handler(event, data)
|
||||||
|
|
||||||
# Allow /start to reach the handler so the check can be re-run.
|
# Allow /start to reach the handler so the check can be re-run.
|
||||||
message_object: Optional[Message] = event.message
|
message_object: Optional[Message] = event.message
|
||||||
if (
|
if (
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ class FreeKassaService:
|
|||||||
self.second_secret: Optional[str] = settings.FREEKASSA_SECOND_SECRET
|
self.second_secret: Optional[str] = settings.FREEKASSA_SECOND_SECRET
|
||||||
self.default_currency: str = (settings.DEFAULT_CURRENCY_SYMBOL or "RUB").upper()
|
self.default_currency: str = (settings.DEFAULT_CURRENCY_SYMBOL or "RUB").upper()
|
||||||
self.server_ip: Optional[str] = settings.FREEKASSA_PAYMENT_IP
|
self.server_ip: Optional[str] = settings.FREEKASSA_PAYMENT_IP
|
||||||
|
self.payment_method_id: Optional[int] = settings.FREEKASSA_PAYMENT_METHOD_ID
|
||||||
|
|
||||||
self.api_base_url: str = "https://api.fk.life/v1"
|
self.api_base_url: str = "https://api.fk.life/v1"
|
||||||
self._timeout = ClientTimeout(total=15)
|
self._timeout = ClientTimeout(total=15)
|
||||||
@@ -72,9 +73,9 @@ class FreeKassaService:
|
|||||||
months: int,
|
months: int,
|
||||||
amount: float,
|
amount: float,
|
||||||
currency: Optional[str],
|
currency: Optional[str],
|
||||||
method_code: int,
|
|
||||||
email: Optional[str] = None,
|
email: Optional[str] = None,
|
||||||
ip_address: Optional[str] = None,
|
ip_address: Optional[str] = None,
|
||||||
|
payment_method_id: Optional[int] = None,
|
||||||
extra_params: Optional[Dict[str, Any]] = None,
|
extra_params: Optional[Dict[str, Any]] = None,
|
||||||
) -> Tuple[bool, Dict[str, Any]]:
|
) -> Tuple[bool, Dict[str, Any]]:
|
||||||
if not self.configured:
|
if not self.configured:
|
||||||
@@ -94,7 +95,7 @@ class FreeKassaService:
|
|||||||
"shopId": int(self.shop_id),
|
"shopId": int(self.shop_id),
|
||||||
"nonce": await self._generate_nonce(),
|
"nonce": await self._generate_nonce(),
|
||||||
"paymentId": str(payment_db_id),
|
"paymentId": str(payment_db_id),
|
||||||
"i": int(method_code),
|
"i": int(payment_method_id),
|
||||||
"amount": amount_str,
|
"amount": amount_str,
|
||||||
"currency": currency_code,
|
"currency": currency_code,
|
||||||
"email": email,
|
"email": email,
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ class Settings(BaseSettings):
|
|||||||
FREEKASSA_PAYMENT_URL: str = Field(default="https://pay.freekassa.ru/")
|
FREEKASSA_PAYMENT_URL: str = Field(default="https://pay.freekassa.ru/")
|
||||||
FREEKASSA_API_KEY: Optional[str] = None
|
FREEKASSA_API_KEY: Optional[str] = None
|
||||||
FREEKASSA_PAYMENT_IP: Optional[str] = None
|
FREEKASSA_PAYMENT_IP: Optional[str] = None
|
||||||
|
FREEKASSA_PAYMENT_METHOD_ID: Optional[int] = None
|
||||||
|
|
||||||
YOOKASSA_ENABLED: bool = Field(default=True)
|
YOOKASSA_ENABLED: bool = Field(default=True)
|
||||||
STARS_ENABLED: bool = Field(default=True)
|
STARS_ENABLED: bool = Field(default=True)
|
||||||
|
|||||||
@@ -262,3 +262,37 @@ async def get_last_tribute_payment(
|
|||||||
Payment.created_at.desc()).limit(1))
|
Payment.created_at.desc()).limit(1))
|
||||||
result = await session.execute(stmt)
|
result = await session.execute(stmt)
|
||||||
return result.scalar_one_or_none()
|
return result.scalar_one_or_none()
|
||||||
|
|
||||||
|
|
||||||
|
async def get_user_total_paid(session: AsyncSession, user_id: int) -> float:
|
||||||
|
"""Get total amount paid by a specific user (sum of all succeeded payments)."""
|
||||||
|
stmt = select(func.sum(Payment.amount)).where(
|
||||||
|
and_(
|
||||||
|
Payment.user_id == user_id,
|
||||||
|
Payment.status == 'succeeded'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
result = await session.execute(stmt)
|
||||||
|
total = result.scalar()
|
||||||
|
return float(total or 0)
|
||||||
|
|
||||||
|
|
||||||
|
async def get_referral_revenue(session: AsyncSession, referrer_id: int) -> float:
|
||||||
|
"""Get total revenue generated from referred users' payments.
|
||||||
|
|
||||||
|
This calculates the sum of all succeeded payments made by users
|
||||||
|
where referred_by_id equals the referrer_id.
|
||||||
|
"""
|
||||||
|
from db.models import User
|
||||||
|
|
||||||
|
stmt = select(func.sum(Payment.amount)).join(
|
||||||
|
User, Payment.user_id == User.user_id
|
||||||
|
).where(
|
||||||
|
and_(
|
||||||
|
User.referred_by_id == referrer_id,
|
||||||
|
Payment.status == 'succeeded'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
result = await session.execute(stmt)
|
||||||
|
total = result.scalar()
|
||||||
|
return float(total or 0)
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
services:
|
||||||
|
remnawave-tg-shop:
|
||||||
|
image: ghcr.io/machka-pasla/remnawave-tg-shop:latest
|
||||||
|
container_name: remnawave-tg-shop
|
||||||
|
hostname: remnawave-tg-shop
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
ports:
|
||||||
|
- 8080:8080
|
||||||
|
networks:
|
||||||
|
- remnawave-network
|
||||||
|
environment:
|
||||||
|
- TZ=UTC
|
||||||
|
volumes:
|
||||||
|
- ./locales:/app/locales
|
||||||
|
restart: unless-stopped
|
||||||
|
depends_on:
|
||||||
|
- remnawave-tg-shop-db
|
||||||
|
remnawave-tg-shop-db:
|
||||||
|
image: postgres:17
|
||||||
|
container_name: remnawave-tg-shop-db
|
||||||
|
hostname: remnawave-tg-shop-db
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
environment:
|
||||||
|
- TZ=UTC
|
||||||
|
volumes:
|
||||||
|
- remnawave-tg-shop-db-data:/var/lib/postgresql/data
|
||||||
|
networks:
|
||||||
|
- remnawave-network
|
||||||
|
restart: unless-stopped
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB"]
|
||||||
|
interval: 5s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 20
|
||||||
|
networks:
|
||||||
|
remnawave-network: null
|
||||||
|
volumes:
|
||||||
|
remnawave-tg-shop-db-data:
|
||||||
|
name: remnawave-tg-shop-db-data
|
||||||
@@ -337,6 +337,8 @@
|
|||||||
"admin_user_na_value": "N/A",
|
"admin_user_na_value": "N/A",
|
||||||
"admin_user_subscription_none": "No active subscription",
|
"admin_user_subscription_none": "No active subscription",
|
||||||
"admin_user_actions_count_label": "📜 <b>Total actions:</b>",
|
"admin_user_actions_count_label": "📜 <b>Total actions:</b>",
|
||||||
|
"admin_user_total_paid_label": "💰 <b>Total Paid:</b>",
|
||||||
|
"admin_user_referral_revenue_label": "💸 <b>Referral Revenue:</b>",
|
||||||
"admin_user_invited_friends_label": "👥 <b>Friends invited:</b>",
|
"admin_user_invited_friends_label": "👥 <b>Friends invited:</b>",
|
||||||
"admin_user_ref_purchased_label": "💳 <b>Purchased subscription:</b>",
|
"admin_user_ref_purchased_label": "💳 <b>Purchased subscription:</b>",
|
||||||
"admin_user_subscription_active_until": "⏰ <b>Active until:</b>",
|
"admin_user_subscription_active_until": "⏰ <b>Active until:</b>",
|
||||||
|
|||||||
@@ -337,6 +337,8 @@
|
|||||||
"admin_user_na_value": "N/A",
|
"admin_user_na_value": "N/A",
|
||||||
"admin_user_subscription_none": "Нет активной подписки",
|
"admin_user_subscription_none": "Нет активной подписки",
|
||||||
"admin_user_actions_count_label": "📜 <b>Всего действий:</b>",
|
"admin_user_actions_count_label": "📜 <b>Всего действий:</b>",
|
||||||
|
"admin_user_total_paid_label": "💰 <b>Всего оплачено:</b>",
|
||||||
|
"admin_user_referral_revenue_label": "💸 <b>Доход по рефералам:</b>",
|
||||||
"admin_user_invited_friends_label": "👥 <b>Приглашено друзей:</b>",
|
"admin_user_invited_friends_label": "👥 <b>Приглашено друзей:</b>",
|
||||||
"admin_user_ref_purchased_label": "💳 <b>Купили подписку:</b>",
|
"admin_user_ref_purchased_label": "💳 <b>Купили подписку:</b>",
|
||||||
"admin_user_subscription_active_until": "⏰ <b>Действует до:</b>",
|
"admin_user_subscription_active_until": "⏰ <b>Действует до:</b>",
|
||||||
|
|||||||
Reference in New Issue
Block a user