events & global sync. unstable

This commit is contained in:
unexpected committed 2025-10-15 16:57:21 +00:00
1 parent 77921ba6a8
commit f140181c45
21 files changed
+1196 -31

No files matched your search

+6
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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')
+77
View File
@@ -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)
+21 -1
View File
@@ -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