Поправлена интеграция с битриксом
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user