events & global sync. unstable
This commit is contained in:
1 parent
77921ba6a8
commit
f140181c45
21 files changed
+1196
-31
No files matched your search
@@ -20,6 +20,7 @@ from app.api.routes.network import (
|
||||
s_api_v1_network_nodes,
|
||||
s_api_v1_network_handshake,
|
||||
)
|
||||
from app.api.routes.network_events import s_api_v1_network_events
|
||||
from app.api.routes.auth import s_api_v1_auth_twa, s_api_v1_auth_select_wallet, s_api_v1_auth_me
|
||||
from app.api.routes.statics import s_api_tonconnect_manifest, s_api_platform_metadata
|
||||
from app.api.routes.node_storage import s_api_v1_storage_post, s_api_v1_storage_get, \
|
||||
@@ -36,9 +37,11 @@ from app.api.routes.admin import (
|
||||
s_api_v1_admin_blockchain,
|
||||
s_api_v1_admin_cache_cleanup,
|
||||
s_api_v1_admin_cache_setlimits,
|
||||
s_api_v1_admin_events,
|
||||
s_api_v1_admin_licenses,
|
||||
s_api_v1_admin_login,
|
||||
s_api_v1_admin_logout,
|
||||
s_api_v1_admin_users_setadmin,
|
||||
s_api_v1_admin_node_setrole,
|
||||
s_api_v1_admin_nodes,
|
||||
s_api_v1_admin_overview,
|
||||
@@ -66,6 +69,7 @@ app.add_route(s_api_system_send_status, "/api/system.sendStatus", methods=["POST
|
||||
app.add_route(s_api_v1_network_info, "/api/v1/network.info", methods=["GET", "OPTIONS"])
|
||||
app.add_route(s_api_v1_network_nodes, "/api/v1/network.nodes", methods=["GET", "OPTIONS"])
|
||||
app.add_route(s_api_v1_network_handshake, "/api/v1/network.handshake", methods=["POST", "OPTIONS"])
|
||||
app.add_route(s_api_v1_network_events, "/api/v1/network.events", methods=["GET", "OPTIONS"])
|
||||
|
||||
app.add_route(s_api_tonconnect_manifest, "/api/tonconnect-manifest.json", methods=["GET", "OPTIONS"])
|
||||
app.add_route(s_api_platform_metadata, "/api/platform-metadata.json", methods=["GET", "OPTIONS"])
|
||||
@@ -102,8 +106,10 @@ app.add_route(s_api_v1_admin_overview, "/api/v1/admin.overview", methods=["GET",
|
||||
app.add_route(s_api_v1_admin_storage, "/api/v1/admin.storage", methods=["GET", "OPTIONS"])
|
||||
app.add_route(s_api_v1_admin_uploads, "/api/v1/admin.uploads", methods=["GET", "OPTIONS"])
|
||||
app.add_route(s_api_v1_admin_users, "/api/v1/admin.users", methods=["GET", "OPTIONS"])
|
||||
app.add_route(s_api_v1_admin_users_setadmin, "/api/v1/admin.users.setAdmin", methods=["POST", "OPTIONS"])
|
||||
app.add_route(s_api_v1_admin_licenses, "/api/v1/admin.licenses", methods=["GET", "OPTIONS"])
|
||||
app.add_route(s_api_v1_admin_stars, "/api/v1/admin.stars", methods=["GET", "OPTIONS"])
|
||||
app.add_route(s_api_v1_admin_events, "/api/v1/admin.events", methods=["GET", "OPTIONS"])
|
||||
app.add_route(s_api_v1_admin_system, "/api/v1/admin.system", methods=["GET", "OPTIONS"])
|
||||
app.add_route(s_api_v1_admin_blockchain, "/api/v1/admin.blockchain", methods=["GET", "OPTIONS"])
|
||||
app.add_route(s_api_v1_admin_node_setrole, "/api/v1/admin.node.setRole", methods=["POST", "OPTIONS"])
|
||||
|
||||
+327
-3
@@ -7,11 +7,12 @@ import shutil
|
||||
from collections import defaultdict
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional
|
||||
from typing import Any, Dict, List, Optional, Tuple
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from base58 import b58encode
|
||||
from sanic import response
|
||||
from sqlalchemy import Integer, String, and_, case, cast, func, or_, select
|
||||
from sqlalchemy import Integer, String, and_, case, cast, func, or_, select, Text
|
||||
|
||||
from app.api.routes._system import get_git_info
|
||||
from app.core._blockchain.ton.platform import platform
|
||||
@@ -20,6 +21,7 @@ from app.core._config import (
|
||||
BACKEND_LOGS_DIR_HOST,
|
||||
LOG_DIR,
|
||||
CLIENT_TELEGRAM_BOT_USERNAME,
|
||||
TELEGRAM_BOT_USERNAME,
|
||||
PROJECT_HOST,
|
||||
UPLOADS_DIR,
|
||||
)
|
||||
@@ -34,7 +36,8 @@ from app.core.models.content_v3 import (
|
||||
IpfsSync,
|
||||
UploadSession,
|
||||
)
|
||||
from app.core.models.my_network import KnownNode
|
||||
from app.core.models.my_network import KnownNode, RemoteContentIndex
|
||||
from app.core.models.events import NodeEvent
|
||||
from app.core.models.tasks import BlockchainTask
|
||||
from app.core.models.node_storage import StoredContent
|
||||
from app.core.models.user import User
|
||||
@@ -44,6 +47,7 @@ from app.core.models.wallet_connection import WalletConnection
|
||||
from app.core.models.user_activity import UserActivity
|
||||
from app.core._utils.share_links import build_content_links
|
||||
from app.core.content.content_id import ContentId
|
||||
from app.core.events.service import record_event
|
||||
|
||||
MIN_ONCHAIN_INDEX = int(os.getenv("MIN_ONCHAIN_INDEX", "8"))
|
||||
|
||||
@@ -161,6 +165,38 @@ def _service_states(request) -> List[Dict[str, Any]]:
|
||||
return items
|
||||
|
||||
|
||||
def _node_public_base(node: KnownNode) -> Optional[str]:
|
||||
meta = node.meta or {}
|
||||
public_host = (meta.get('public_host') or '').strip()
|
||||
if public_host:
|
||||
base = public_host.rstrip('/')
|
||||
if base.startswith('http://') or base.startswith('https://'):
|
||||
return base
|
||||
scheme = 'https' if node.port == 443 else 'http'
|
||||
return f"{scheme}://{base.lstrip('/')}"
|
||||
scheme = 'https' if node.port == 443 else 'http'
|
||||
host = (node.ip or '').strip()
|
||||
if not host:
|
||||
return None
|
||||
default_port = 443 if scheme == 'https' else 80
|
||||
if node.port and node.port != default_port:
|
||||
return f"{scheme}://{host}:{node.port}"
|
||||
return f"{scheme}://{host}"
|
||||
|
||||
|
||||
def _node_gateway_base(node: KnownNode) -> Optional[str]:
|
||||
meta = node.meta or {}
|
||||
public_host = meta.get('public_host') or node.ip or ''
|
||||
if not public_host:
|
||||
return None
|
||||
parsed = urlparse(public_host if '://' in public_host else f"https://{public_host}")
|
||||
hostname = parsed.hostname or (node.ip or '').strip()
|
||||
if not hostname:
|
||||
return None
|
||||
port = parsed.port or 8080
|
||||
return f"http://{hostname}:{port}"
|
||||
|
||||
|
||||
def _format_dt(value: Optional[datetime]) -> Optional[str]:
|
||||
return value.isoformat() + 'Z' if isinstance(value, datetime) else None
|
||||
|
||||
@@ -478,6 +514,18 @@ async def s_api_v1_admin_uploads(request):
|
||||
for content_id, count in license_rows:
|
||||
license_counts[int(content_id)] = int(count)
|
||||
|
||||
remote_map: Dict[str, List[Tuple[RemoteContentIndex, KnownNode]]] = defaultdict(list)
|
||||
if encrypted_cids:
|
||||
remote_rows = (await session.execute(
|
||||
select(RemoteContentIndex, KnownNode)
|
||||
.join(KnownNode, RemoteContentIndex.remote_node_id == KnownNode.id)
|
||||
.where(RemoteContentIndex.encrypted_hash.in_(encrypted_cids))
|
||||
)).all()
|
||||
for remote_row, node in remote_rows:
|
||||
if not remote_row.encrypted_hash:
|
||||
continue
|
||||
remote_map[remote_row.encrypted_hash].append((remote_row, node))
|
||||
|
||||
contents_payload: List[Dict[str, Any]] = []
|
||||
category_totals: Dict[str, int] = {key: 0 for key in ALLOWED_UPLOAD_FILTERS if key != 'all'}
|
||||
matched_total = 0
|
||||
@@ -599,6 +647,66 @@ async def s_api_v1_admin_uploads(request):
|
||||
if url
|
||||
]
|
||||
|
||||
distribution_nodes: List[Dict[str, Any]] = []
|
||||
meta_local_host = urlparse(PROJECT_HOST) if PROJECT_HOST else None
|
||||
if stored:
|
||||
distribution_nodes.append({
|
||||
'node_id': None,
|
||||
'is_local': True,
|
||||
'host': (meta_local_host.hostname if meta_local_host and meta_local_host.hostname else 'local'),
|
||||
'public_host': PROJECT_HOST.rstrip('/') if PROJECT_HOST else None,
|
||||
'version': None,
|
||||
'role': 'self',
|
||||
'last_seen': None,
|
||||
'content': {
|
||||
'encrypted_cid': content.encrypted_cid,
|
||||
'content_type': content.content_type,
|
||||
'size_bytes': content.enc_size_bytes,
|
||||
'preview_enabled': content.preview_enabled,
|
||||
'updated_at': _format_dt(content.updated_at),
|
||||
'metadata_cid': metadata_cid,
|
||||
'issuer_node_id': None,
|
||||
},
|
||||
'links': {
|
||||
'web_view': web_view_url,
|
||||
'api_view': f"{PROJECT_HOST}/api/v1/content.view/{share_target}" if PROJECT_HOST else None,
|
||||
'gateway_view': None,
|
||||
},
|
||||
})
|
||||
|
||||
remote_entries = remote_map.get(content.encrypted_cid, [])
|
||||
for remote_row, node in remote_entries:
|
||||
node_meta = node.meta or {}
|
||||
base_url = _node_public_base(node)
|
||||
gateway_base = _node_gateway_base(node)
|
||||
remote_meta = remote_row.meta if isinstance(remote_row.meta, dict) else {}
|
||||
remote_share_target = remote_meta.get('share_target') or content.encrypted_cid
|
||||
distribution_nodes.append({
|
||||
'node_id': node.id,
|
||||
'is_local': False,
|
||||
'host': node.ip,
|
||||
'public_host': node_meta.get('public_host'),
|
||||
'version': node_meta.get('version'),
|
||||
'role': node_meta.get('role') or 'read-only',
|
||||
'last_seen': _format_dt(node.last_sync),
|
||||
'content': {
|
||||
'encrypted_cid': remote_row.encrypted_hash,
|
||||
'content_type': remote_row.content_type,
|
||||
'size_bytes': remote_meta.get('size_bytes'),
|
||||
'preview_enabled': remote_meta.get('preview_enabled'),
|
||||
'updated_at': _format_dt(remote_row.last_updated),
|
||||
'metadata_cid': remote_meta.get('metadata_cid'),
|
||||
'issuer_node_id': remote_meta.get('issuer_node_id'),
|
||||
},
|
||||
'links': {
|
||||
'web_view': f"{base_url}/viewContent?content={remote_share_target}" if base_url else None,
|
||||
'api_view': f"{base_url}/api/v1/content.view/{remote_share_target}" if base_url else None,
|
||||
'gateway_view': f"{gateway_base}/ipfs/{content.encrypted_cid}" if gateway_base else None,
|
||||
},
|
||||
})
|
||||
if len(distribution_nodes) > 1:
|
||||
distribution_nodes.sort(key=lambda entry: (0 if entry.get('is_local') else 1, entry.get('host') or ''))
|
||||
|
||||
upload_state_norm = (latest_upload.state or '').lower() if latest_upload else ''
|
||||
conversion_state_norm = (conversion_state or '').lower() if conversion_state else ''
|
||||
ipfs_state_norm = (ipfs_sync.pin_state or '').lower() if (ipfs_sync and ipfs_sync.pin_state) else ''
|
||||
@@ -733,6 +841,10 @@ async def s_api_v1_admin_uploads(request):
|
||||
'download_primary': primary_download,
|
||||
'download_derivatives': derivative_downloads,
|
||||
},
|
||||
'distribution': {
|
||||
'local_present': bool(stored),
|
||||
'nodes': distribution_nodes,
|
||||
},
|
||||
'flags': flags,
|
||||
})
|
||||
|
||||
@@ -821,6 +933,7 @@ async def s_api_v1_admin_users(request):
|
||||
'items': [],
|
||||
'summary': {
|
||||
'users_returned': 0,
|
||||
'admins_total': 0,
|
||||
'wallets_total': 0,
|
||||
'wallets_active': 0,
|
||||
'licenses_total': 0,
|
||||
@@ -828,6 +941,7 @@ async def s_api_v1_admin_users(request):
|
||||
'stars_total': 0,
|
||||
'stars_paid': 0,
|
||||
'stars_unpaid': 0,
|
||||
'stars_amount_total': 0,
|
||||
'stars_amount_paid': 0,
|
||||
'stars_amount_unpaid': 0,
|
||||
'unique_ips_total': 0,
|
||||
@@ -994,6 +1108,7 @@ async def s_api_v1_admin_users(request):
|
||||
items: List[Dict[str, Any]] = []
|
||||
summary = {
|
||||
'users_returned': 0,
|
||||
'admins_total': 0,
|
||||
'wallets_total': 0,
|
||||
'wallets_active': 0,
|
||||
'licenses_total': 0,
|
||||
@@ -1009,6 +1124,8 @@ async def s_api_v1_admin_users(request):
|
||||
|
||||
for user in user_rows:
|
||||
summary['users_returned'] += 1
|
||||
if getattr(user, 'is_admin', False):
|
||||
summary['admins_total'] += 1
|
||||
meta = user.meta or {}
|
||||
|
||||
wallet_list = wallet_map.get(user.id, [])
|
||||
@@ -1077,6 +1194,7 @@ async def s_api_v1_admin_users(request):
|
||||
'created_at': _format_dt(user.created),
|
||||
'updated_at': _format_dt(user.updated),
|
||||
'last_use': _format_dt(user.last_use),
|
||||
'is_admin': bool(user.is_admin),
|
||||
'meta': {
|
||||
'ref_id': meta.get('ref_id'),
|
||||
'referrer_id': meta.get('referrer_id'),
|
||||
@@ -1111,6 +1229,59 @@ async def s_api_v1_admin_users(request):
|
||||
return response.json(base_payload)
|
||||
|
||||
|
||||
async def s_api_v1_admin_users_setadmin(request):
|
||||
if (unauth := _ensure_admin(request)):
|
||||
return unauth
|
||||
|
||||
data = request.json or {}
|
||||
try:
|
||||
user_id = int(data.get('user_id'))
|
||||
except (TypeError, ValueError):
|
||||
return response.json({"error": "BAD_USER_ID"}, status=400)
|
||||
|
||||
is_admin_raw = data.get('is_admin')
|
||||
if isinstance(is_admin_raw, str):
|
||||
normalized = is_admin_raw.strip().lower()
|
||||
if normalized in {'1', 'true', 'yes', 'y', 'on'}:
|
||||
is_admin = True
|
||||
elif normalized in {'0', 'false', 'no', 'n', 'off'}:
|
||||
is_admin = False
|
||||
else:
|
||||
return response.json({"error": "BAD_FLAG"}, status=400)
|
||||
else:
|
||||
is_admin = bool(is_admin_raw)
|
||||
|
||||
session = request.ctx.db_session
|
||||
user = await session.get(User, user_id)
|
||||
if not user:
|
||||
return response.json({"error": "NOT_FOUND"}, status=404)
|
||||
|
||||
user.is_admin = is_admin
|
||||
user.updated = datetime.utcnow()
|
||||
try:
|
||||
await record_event(
|
||||
session,
|
||||
'user_role_changed',
|
||||
{
|
||||
'user_id': user.id,
|
||||
'telegram_id': user.telegram_id,
|
||||
'is_admin': is_admin,
|
||||
},
|
||||
origin_host=PROJECT_HOST,
|
||||
)
|
||||
except Exception as exc:
|
||||
make_log('Admin', f"Failed to record user_role_changed event: {exc}", level='warning')
|
||||
|
||||
await session.commit()
|
||||
return response.json({
|
||||
"ok": True,
|
||||
"user": {
|
||||
"id": user.id,
|
||||
"is_admin": bool(user.is_admin),
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
async def s_api_v1_admin_licenses(request):
|
||||
if (unauth := _ensure_admin(request)):
|
||||
return unauth
|
||||
@@ -1597,6 +1768,15 @@ async def s_api_v1_admin_stars(request):
|
||||
'amount': invoice.amount,
|
||||
'paid': bool(invoice.paid),
|
||||
'invoice_url': invoice.invoice_url,
|
||||
'telegram_id': invoice.telegram_id,
|
||||
'bot_username': invoice.bot_username,
|
||||
'payment_node': {
|
||||
'public_key': invoice.payment_node_id,
|
||||
'host': invoice.payment_node_public_host,
|
||||
},
|
||||
'payment_tx_id': invoice.payment_tx_id,
|
||||
'paid_at': _format_dt(invoice.paid_at),
|
||||
'is_remote': bool(invoice.is_remote),
|
||||
'created_at': _format_dt(invoice.created),
|
||||
'user': user_payload,
|
||||
'content': content_payload,
|
||||
@@ -1618,6 +1798,136 @@ async def s_api_v1_admin_stars(request):
|
||||
return response.json(base_payload)
|
||||
|
||||
|
||||
async def s_api_v1_admin_events(request):
|
||||
if (unauth := _ensure_admin(request)):
|
||||
return unauth
|
||||
|
||||
session = request.ctx.db_session
|
||||
|
||||
try:
|
||||
limit = int(request.args.get('limit') or 50)
|
||||
except (TypeError, ValueError):
|
||||
limit = 50
|
||||
limit = max(1, min(limit, 200))
|
||||
|
||||
try:
|
||||
offset = int(request.args.get('offset') or 0)
|
||||
except (TypeError, ValueError):
|
||||
offset = 0
|
||||
offset = max(0, offset)
|
||||
|
||||
type_param = (request.args.get('type') or '').strip()
|
||||
status_param = (request.args.get('status') or '').strip()
|
||||
origin_param = (request.args.get('origin') or '').strip()
|
||||
search_param = (request.args.get('search') or '').strip()
|
||||
|
||||
filters = []
|
||||
applied_filters: Dict[str, Any] = {}
|
||||
|
||||
if type_param:
|
||||
type_values = [value.strip() for value in type_param.split(',') if value.strip()]
|
||||
if type_values:
|
||||
filters.append(NodeEvent.event_type.in_(type_values))
|
||||
applied_filters['type'] = type_values
|
||||
|
||||
if status_param:
|
||||
status_values = [value.strip() for value in status_param.split(',') if value.strip()]
|
||||
if status_values:
|
||||
filters.append(NodeEvent.status.in_(status_values))
|
||||
applied_filters['status'] = status_values
|
||||
|
||||
if origin_param:
|
||||
origin_values = [value.strip() for value in origin_param.split(',') if value.strip()]
|
||||
if origin_values:
|
||||
filters.append(NodeEvent.origin_public_key.in_(origin_values))
|
||||
applied_filters['origin'] = origin_values
|
||||
|
||||
if search_param:
|
||||
search_like = f"%{search_param}%"
|
||||
filters.append(or_(
|
||||
NodeEvent.uid.ilike(search_like),
|
||||
cast(NodeEvent.payload, Text).ilike(search_like),
|
||||
))
|
||||
applied_filters['search'] = search_param
|
||||
|
||||
total_stmt = select(func.count()).select_from(NodeEvent)
|
||||
if filters:
|
||||
total_stmt = total_stmt.where(and_(*filters))
|
||||
total = (await session.execute(total_stmt)).scalar_one()
|
||||
|
||||
query_stmt = (
|
||||
select(NodeEvent)
|
||||
.order_by(NodeEvent.created_at.desc())
|
||||
.offset(offset)
|
||||
.limit(limit)
|
||||
)
|
||||
if filters:
|
||||
query_stmt = query_stmt.where(and_(*filters))
|
||||
|
||||
rows = (await session.execute(query_stmt)).scalars().all()
|
||||
|
||||
def _event_links(row: NodeEvent) -> Dict[str, Optional[str]]:
|
||||
links: Dict[str, Optional[str]] = {}
|
||||
payload = row.payload or {}
|
||||
cid = payload.get('encrypted_cid') or payload.get('content_cid') or payload.get('content_id')
|
||||
if cid:
|
||||
links['admin_uploads'] = f"uploads?search={cid}"
|
||||
if PROJECT_HOST:
|
||||
links['content_view'] = f"{PROJECT_HOST}/viewContent?content={cid}"
|
||||
invoice_id = payload.get('invoice_id')
|
||||
if invoice_id:
|
||||
links['admin_stars'] = f"stars?search={invoice_id}"
|
||||
user_id = payload.get('user_id')
|
||||
telegram_id = payload.get('telegram_id')
|
||||
if user_id:
|
||||
links['admin_user'] = f"users?search={user_id}"
|
||||
elif telegram_id:
|
||||
links['admin_user'] = f"users?search={telegram_id}"
|
||||
return links
|
||||
|
||||
items: List[Dict[str, Any]] = []
|
||||
for row in rows:
|
||||
items.append({
|
||||
'id': row.id,
|
||||
'origin_public_key': row.origin_public_key,
|
||||
'origin_host': row.origin_host,
|
||||
'seq': int(row.seq),
|
||||
'uid': row.uid,
|
||||
'event_type': row.event_type,
|
||||
'status': row.status,
|
||||
'created_at': _format_dt(row.created_at),
|
||||
'received_at': _format_dt(row.received_at),
|
||||
'applied_at': _format_dt(row.applied_at),
|
||||
'payload': row.payload or {},
|
||||
'links': _event_links(row),
|
||||
})
|
||||
|
||||
type_stmt = select(NodeEvent.event_type, func.count()).group_by(NodeEvent.event_type)
|
||||
status_stmt = select(NodeEvent.status, func.count()).group_by(NodeEvent.status)
|
||||
origin_stmt = select(NodeEvent.origin_public_key, func.count()).group_by(NodeEvent.origin_public_key)
|
||||
if filters:
|
||||
type_stmt = type_stmt.where(and_(*filters))
|
||||
status_stmt = status_stmt.where(and_(*filters))
|
||||
origin_stmt = origin_stmt.where(and_(*filters))
|
||||
type_rows = (await session.execute(type_stmt)).all()
|
||||
status_rows = (await session.execute(status_stmt)).all()
|
||||
origin_rows = (await session.execute(origin_stmt)).all()
|
||||
|
||||
payload = {
|
||||
'total': int(total or 0),
|
||||
'limit': limit,
|
||||
'offset': offset,
|
||||
'filters': applied_filters,
|
||||
'items': items,
|
||||
'available_filters': {
|
||||
'types': {event_type or 'unknown': int(count or 0) for event_type, count in type_rows},
|
||||
'statuses': {status or 'unknown': int(count or 0) for status, count in status_rows},
|
||||
'origins': {origin or 'unknown': int(count or 0) for origin, count in origin_rows},
|
||||
}
|
||||
}
|
||||
return response.json(payload)
|
||||
|
||||
|
||||
async def s_api_v1_admin_system(request):
|
||||
if (unauth := _ensure_admin(request)):
|
||||
return unauth
|
||||
@@ -1643,6 +1953,19 @@ async def s_api_v1_admin_system(request):
|
||||
'LOG_LEVEL': os.getenv('LOG_LEVEL'),
|
||||
'TESTNET': os.getenv('TESTNET'),
|
||||
}
|
||||
telegram_bots: List[Dict[str, Any]] = []
|
||||
if TELEGRAM_BOT_USERNAME:
|
||||
telegram_bots.append({
|
||||
'role': 'uploader',
|
||||
'username': TELEGRAM_BOT_USERNAME,
|
||||
'url': f'https://t.me/{TELEGRAM_BOT_USERNAME}',
|
||||
})
|
||||
if CLIENT_TELEGRAM_BOT_USERNAME:
|
||||
telegram_bots.append({
|
||||
'role': 'client',
|
||||
'username': CLIENT_TELEGRAM_BOT_USERNAME,
|
||||
'url': f'https://t.me/{CLIENT_TELEGRAM_BOT_USERNAME}',
|
||||
})
|
||||
|
||||
blockchain_counts_rows = (await session.execute(
|
||||
select(BlockchainTask.status, func.count()).group_by(BlockchainTask.status)
|
||||
@@ -1666,6 +1989,7 @@ async def s_api_v1_admin_system(request):
|
||||
'services': _service_states(request),
|
||||
'blockchain_tasks': blockchain_counts,
|
||||
'latest_index_items': index_entries,
|
||||
'telegram_bots': telegram_bots,
|
||||
}
|
||||
return response.json(payload)
|
||||
|
||||
|
||||
+23
-11
@@ -1,6 +1,6 @@
|
||||
from datetime import datetime, timedelta
|
||||
from sanic import response
|
||||
from sqlalchemy import select, and_, func
|
||||
from sqlalchemy import select, and_, func, or_
|
||||
from aiogram import Bot, types
|
||||
from sqlalchemy import and_
|
||||
from app.core.logger import make_log
|
||||
@@ -9,7 +9,7 @@ from app.core.models.node_storage import StoredContent
|
||||
from app.core.models.keys import KnownKey
|
||||
from app.core.models import StarsInvoice
|
||||
from app.core.models.content.user_content import UserContent
|
||||
from app.core._config import CLIENT_TELEGRAM_API_KEY, PROJECT_HOST
|
||||
from app.core._config import CLIENT_TELEGRAM_API_KEY, CLIENT_TELEGRAM_BOT_USERNAME, PROJECT_HOST
|
||||
from app.core.models.content_v3 import EncryptedContent as ECv3, ContentDerivative as CDv3, UploadSession
|
||||
from app.core.content.content_id import ContentId
|
||||
import json
|
||||
@@ -103,18 +103,26 @@ async def s_api_v1_content_view(request, content_address: str):
|
||||
have_access = False
|
||||
if request.ctx.user:
|
||||
user_wallet_address = await request.ctx.user.wallet_address_async(request.ctx.db_session)
|
||||
user_telegram_id = getattr(request.ctx.user, 'telegram_id', None)
|
||||
or_clauses = [StarsInvoice.user_id == request.ctx.user.id]
|
||||
if user_telegram_id is not None:
|
||||
or_clauses.append(StarsInvoice.telegram_id == user_telegram_id)
|
||||
stars_access = False
|
||||
if or_clauses:
|
||||
stars_access = bool((await request.ctx.db_session.execute(select(StarsInvoice).where(
|
||||
and_(
|
||||
StarsInvoice.content_hash == content['encrypted_content'].hash,
|
||||
StarsInvoice.paid.is_(True),
|
||||
or_(*or_clauses)
|
||||
)
|
||||
))).scalars().first())
|
||||
|
||||
have_access = (
|
||||
(content['encrypted_content'].owner_address == user_wallet_address)
|
||||
or bool((await request.ctx.db_session.execute(select(UserContent).where(
|
||||
and_(UserContent.owner_address == user_wallet_address, UserContent.status == 'active', UserContent.content_id == content['encrypted_content'].id)
|
||||
))).scalars().first()) \
|
||||
or bool((await request.ctx.db_session.execute(select(StarsInvoice).where(
|
||||
and_(
|
||||
StarsInvoice.user_id == request.ctx.user.id,
|
||||
StarsInvoice.content_hash == content['encrypted_content'].hash,
|
||||
StarsInvoice.paid == True
|
||||
)
|
||||
))).scalars().first())
|
||||
or stars_access
|
||||
)
|
||||
|
||||
if not have_access:
|
||||
@@ -123,8 +131,10 @@ async def s_api_v1_content_view(request, content_address: str):
|
||||
current_star_rate = 0.00000001
|
||||
|
||||
stars_cost = int(int(content['encrypted_content'].meta['license']['resale']['price']) / 1e9 / current_star_rate * 1.2)
|
||||
if request.ctx.user.telegram_id in [5587262915, 6861699286]:
|
||||
if getattr(request.ctx.user, 'is_admin', False):
|
||||
stars_cost = 2
|
||||
else:
|
||||
stars_cost = int(int(content['encrypted_content'].meta['license']['resale']['price']) / 1e9 / current_star_rate * 1.2)
|
||||
|
||||
invoice_id = f"access_{uuid.uuid4().hex}"
|
||||
exist_invoice = (await request.ctx.db_session.execute(select(StarsInvoice).where(
|
||||
@@ -155,7 +165,9 @@ async def s_api_v1_content_view(request, content_address: str):
|
||||
amount=stars_cost,
|
||||
user_id=request.ctx.user.id,
|
||||
content_hash=content['encrypted_content'].hash,
|
||||
invoice_url=invoice_url
|
||||
invoice_url=invoice_url,
|
||||
telegram_id=getattr(request.ctx.user, 'telegram_id', None),
|
||||
bot_username=CLIENT_TELEGRAM_BOT_USERNAME,
|
||||
)
|
||||
)
|
||||
await request.ctx.db_session.commit()
|
||||
|
||||
@@ -17,6 +17,8 @@ from app.core.network.semver import compatibility
|
||||
from app.core.network.guard import check_rate_limit, check_timestamp_fresh, check_and_remember_nonce
|
||||
from app.core.network.config import HANDSHAKE_TS_TOLERANCE_SEC
|
||||
from app.core.ipfs_client import swarm_connect
|
||||
from app.core._config import PROJECT_HOST
|
||||
from app.core.events.service import record_event
|
||||
|
||||
|
||||
def _port_from_public_host(public_host: str) -> int:
|
||||
@@ -171,6 +173,21 @@ async def s_api_v1_network_handshake(request):
|
||||
}
|
||||
)
|
||||
await _connect_ipfs_multiaddrs(ipfs_meta.get("multiaddrs"))
|
||||
try:
|
||||
await record_event(
|
||||
request.ctx.db_session,
|
||||
'node_registered',
|
||||
{
|
||||
'public_key': str(data.get("public_key")),
|
||||
'public_host': data.get("public_host"),
|
||||
'node_type': data.get("node_type"),
|
||||
'version': peer_version,
|
||||
'capabilities': data.get("capabilities", {}),
|
||||
},
|
||||
origin_host=PROJECT_HOST,
|
||||
)
|
||||
except Exception as ev_exc:
|
||||
make_log("Events", f"Failed to record node_registered event: {ev_exc}", level="warning")
|
||||
except Exception as e:
|
||||
make_log("Handshake", f"Upsert peer failed: {e}", level='warning')
|
||||
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Dict, Any
|
||||
|
||||
from sanic import response
|
||||
from sqlalchemy import select
|
||||
|
||||
from app.core.logger import make_log
|
||||
from app.core.models import NodeEvent, KnownNode
|
||||
from app.core.network.nodesig import verify_request
|
||||
from app.core.network.guard import check_rate_limit
|
||||
from app.core._config import PROJECT_HOST
|
||||
from app.core.events.service import LOCAL_PUBLIC_KEY
|
||||
|
||||
|
||||
def _origin_host() -> str | None:
|
||||
return PROJECT_HOST.rstrip('/') if PROJECT_HOST else None
|
||||
|
||||
|
||||
async def s_api_v1_network_events(request):
|
||||
remote_ip = (request.headers.get('X-Forwarded-For') or request.remote_addr or request.ip or '').split(',')[0].strip()
|
||||
if not check_rate_limit(request.app.ctx.memory, remote_ip):
|
||||
return response.json({"error": "RATE_LIMIT"}, status=429)
|
||||
|
||||
ok, node_id, reason = verify_request(request, request.app.ctx.memory)
|
||||
if not ok:
|
||||
return response.json({"error": reason or "UNAUTHORIZED"}, status=401)
|
||||
|
||||
session = request.ctx.db_session
|
||||
trusted = (await session.execute(
|
||||
select(KnownNode).where(KnownNode.public_key == node_id)
|
||||
)).scalar_one_or_none()
|
||||
role = (trusted.meta or {}).get('role') if trusted and trusted.meta else None
|
||||
if role != 'trusted':
|
||||
make_log("Events", f"Rejected events fetch from non-trusted node {node_id}", level="warning")
|
||||
return response.json({"error": "FORBIDDEN"}, status=403)
|
||||
|
||||
try:
|
||||
since = int(request.args.get('since') or 0)
|
||||
except (TypeError, ValueError):
|
||||
since = 0
|
||||
since = max(since, 0)
|
||||
|
||||
try:
|
||||
limit = int(request.args.get('limit') or 100)
|
||||
except (TypeError, ValueError):
|
||||
limit = 100
|
||||
limit = max(1, min(limit, 200))
|
||||
|
||||
result = await session.execute(
|
||||
select(NodeEvent)
|
||||
.where(NodeEvent.origin_public_key == LOCAL_PUBLIC_KEY, NodeEvent.seq > since)
|
||||
.order_by(NodeEvent.seq.asc())
|
||||
.limit(limit)
|
||||
)
|
||||
rows = result.scalars().all()
|
||||
|
||||
events: list[Dict[str, Any]] = []
|
||||
next_since = since
|
||||
for row in rows:
|
||||
next_since = max(next_since, int(row.seq))
|
||||
events.append({
|
||||
"origin_public_key": row.origin_public_key,
|
||||
"origin_host": row.origin_host or _origin_host(),
|
||||
"seq": int(row.seq),
|
||||
"uid": row.uid,
|
||||
"event_type": row.event_type,
|
||||
"payload": row.payload,
|
||||
"signature": row.signature,
|
||||
"created_at": (row.created_at.isoformat() + 'Z') if row.created_at else None,
|
||||
})
|
||||
|
||||
payload = {
|
||||
"events": events,
|
||||
"next_since": next_since,
|
||||
}
|
||||
return response.json(payload)
|
||||
@@ -10,7 +10,7 @@ import aiofiles
|
||||
from base58 import b58encode
|
||||
from sanic import response
|
||||
|
||||
from app.core._config import UPLOADS_DIR
|
||||
from app.core._config import UPLOADS_DIR, PROJECT_HOST
|
||||
from app.core._secrets import hot_pubkey
|
||||
from app.core.crypto.aes_gcm_stream import encrypt_file_to_encf, CHUNK_BYTES
|
||||
from app.core.crypto.keywrap import wrap_dek, KeyWrapError
|
||||
@@ -20,6 +20,7 @@ from app.core.models.content_v3 import EncryptedContent, ContentKey, IpfsSync, C
|
||||
from app.core.models.node_storage import StoredContent
|
||||
from app.core.storage import db_session
|
||||
from app.core._utils.resolve_content import resolve_content
|
||||
from app.core.events.service import record_event
|
||||
from sqlalchemy import select
|
||||
|
||||
|
||||
@@ -235,6 +236,25 @@ async def s_api_v1_upload_tus_hook(request):
|
||||
sig = ""
|
||||
session.add(ContentIndexItem(encrypted_cid=encrypted_cid, payload=item, sig=sig))
|
||||
|
||||
try:
|
||||
await record_event(
|
||||
session,
|
||||
'content_uploaded',
|
||||
{
|
||||
'encrypted_cid': encrypted_cid,
|
||||
'content_hash': encrypted_hash_b58,
|
||||
'title': title,
|
||||
'description': description,
|
||||
'content_type': content_type,
|
||||
'size_bytes': enc_size,
|
||||
'user_id': request.ctx.user.id if getattr(request.ctx, 'user', None) else None,
|
||||
'telegram_id': getattr(getattr(request.ctx, 'user', None), 'telegram_id', None),
|
||||
},
|
||||
origin_host=PROJECT_HOST,
|
||||
)
|
||||
except Exception as exc:
|
||||
make_log("Events", f"Failed to record content_uploaded event: {exc}", level="warning")
|
||||
|
||||
await session.commit()
|
||||
|
||||
# Update upload session with result and purge staging to avoid duplicates
|
||||
|
||||
Reference in new issue
Block a user