Поправлена интеграция с битриксом

This commit is contained in:
mi
2026-07-16 16:27:16 +03:00
parent 3a3d26bd6e
commit f6726fe4b8
2 changed files with 55 additions and 9 deletions
+52 -8
View File
@@ -12,7 +12,7 @@ import uuid
from contextlib import asynccontextmanager, suppress
from datetime import datetime, timedelta
from typing import Annotated, Any, Literal
from urllib.parse import urlparse
from urllib.parse import quote, urlparse
import httpx
import uvicorn
@@ -40,6 +40,13 @@ from app.postgres import create_postgres_engine
logger = logging.getLogger("bitrix-local-app")
CONNECTOR_ICON_DATA_URI = "data:image/svg+xml," + quote(
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">'
'<path fill="#fff" d="M20 20h15v25h30V20h15v60H65V60H35v20H20z"/>'
"</svg>",
safe="",
)
class Settings(BaseSettings):
model_config = SettingsConfigDict(extra="ignore")
@@ -235,7 +242,11 @@ def normalize_event(payload: dict[str, Any]) -> dict[str, Any] | None:
except (ValueError, TypeError):
raise ValueError("external_chat_id is missing or invalid")
bitrix_message_id = first(
data, ("MESSAGES", "0", "message", "id"), ("MESSAGE", "ID"), ("message_id",)
data,
("MESSAGES", "0", "im", "message_id"),
("MESSAGES", "0", "message", "id"),
("MESSAGE", "ID"),
("message_id",),
)
if event == "ONIMCONNECTORDIALOGFINISH":
event_type = "dialog.closed"
@@ -386,6 +397,10 @@ class BitrixClient:
{
"ID": s.bitrix_connector_id,
"NAME": s.bitrix_connector_name,
"ICON[DATA_IMAGE]": CONNECTOR_ICON_DATA_URI,
"ICON[COLOR]": "#2F80ED",
"ICON[SIZE]": "70%",
"ICON[POSITION]": "center",
"PLACEMENT_HANDLER": f"{s.bitrix_public_base_url}/placement",
},
)
@@ -527,16 +542,15 @@ def create_app(settings: Settings | None = None) -> FastAPI:
event = str(payload.get("event", "")).upper()
if event == "ONAPPINSTALL":
return await install_payload(request.app, payload)
if event == "ONAPPUNINSTALL":
await uninstall_payload(request.app, payload)
return {"status": "uninstalled"}
auth = payload.get("auth") or {}
token = str(auth.get("application_token") or "")
if not hmac.compare_digest(token, cfg.bitrix_application_token):
if not await valid_callback_token(request.app, auth):
raise HTTPException(
403,
safe_error(request.state.request_id, "callback_forbidden", "Invalid callback"),
)
if event == "ONAPPUNINSTALL":
await uninstall_payload(request.app, payload)
return {"status": "uninstalled"}
domain = str(auth.get("domain") or "").lower()
data = payload.get("data") or {}
connector = first(
@@ -760,6 +774,33 @@ async def active_portal(session: AsyncSession) -> PortalInstallation | None:
)
async def valid_callback_token(app: FastAPI, auth: dict[str, Any]) -> bool:
token = str(auth.get("application_token") or auth.get("APPLICATION_TOKEN") or "")
member_id = str(auth.get("member_id") or auth.get("MEMBER_ID") or "")
domain = str(auth.get("domain") or auth.get("DOMAIN") or "").lower()
if not all((token, member_id, domain)):
return False
async with app.state.sessions() as session:
portal = await session.scalar(
select(PortalInstallation).where(
PortalInstallation.member_id == member_id,
PortalInstallation.domain == domain,
PortalInstallation.record_status == "A",
PortalInstallation.install_status == "installed",
)
)
if not portal:
return False
expected = app.state.cipher.decrypt(
portal.application_ciphertext,
portal.application_nonce,
portal.member_id,
portal.domain,
"application",
)
return hmac.compare_digest(token, expected)
async def install_payload(app: FastAPI, payload: dict[str, Any]) -> dict[str, str]:
auth = payload.get("auth") or {}
domain = str(auth.get("domain") or auth.get("DOMAIN") or "").lower()
@@ -967,7 +1008,10 @@ async def reconcile_setup(app: FastAPI) -> dict[str, bool]:
setup.activated = result["activated"]
setup.bindings_json = {"complete": result["bindings"]}
setup.observed_at = now()
setup.attempt_count += 1
# SQLAlchemy applies column defaults during INSERT, not when the Python
# object is constructed. A new setup therefore has None here until it
# is flushed.
setup.attempt_count = (setup.attempt_count or 0) + 1
setup.last_error_code = None if all(result.values()) else "connector_setup_failed"
setup.next_retry_at = (
None