mirror of
https://github.com/langgenius/dify.git
synced 2026-11-10 21:35:45 +08:00
Co-authored-by: zxhlyh <jasonapring2015@outlook.com> Co-authored-by: JzoNg <jzongcode@gmail.com> Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
697 lines
29 KiB
Python
697 lines
29 KiB
Python
"""Canonical Console transport for Human Input Email and IM Channels."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from collections.abc import Sequence
|
|
from enum import StrEnum
|
|
from http import HTTPStatus
|
|
from typing import Annotated, Literal
|
|
from uuid import UUID
|
|
|
|
from flask import request
|
|
from flask.typing import ResponseReturnValue
|
|
from flask_restx import Resource
|
|
from pydantic import Field, StringConstraints
|
|
|
|
from controllers.common.schema import (
|
|
query_params_from_model,
|
|
register_response_schema_models,
|
|
register_schema_models,
|
|
)
|
|
from controllers.console import console_ns
|
|
from controllers.console.wraps import with_current_tenant_id, with_current_user
|
|
from core.human_input_v2.entities import (
|
|
ChannelKind,
|
|
EmailProviderType,
|
|
IMProvider,
|
|
)
|
|
from core.human_input_v2.shared import (
|
|
AccountId,
|
|
EmailProviderId,
|
|
NormalizedEmail,
|
|
TenantId,
|
|
WorkspaceScope,
|
|
)
|
|
from fields.base import ResponseModel
|
|
from fields.timestamp import Timestamp
|
|
from libs.helper import EmailStr
|
|
from models.account import Account
|
|
from repositories.human_input_v2.email_channel import EmailChannelView, EmailConfigurationSnapshot
|
|
from repositories.human_input_v2.email_channel.entities import ResendCandidate
|
|
from repositories.human_input_v2.im_channel_repository import IMChannelId, IMChannelStatus
|
|
from services.human_input_v2.credential_update import ResendChannelUpdate, Unchanged
|
|
from services.human_input_v2.email_channel_management_composition import (
|
|
build_human_input_email_channel_management_service,
|
|
)
|
|
from services.human_input_v2.errors import (
|
|
ChannelAlreadyConfiguredError,
|
|
ChannelNotFoundError,
|
|
ChannelProviderError,
|
|
ProviderConfigurationUpdatedError,
|
|
ReplacementRequiredError,
|
|
)
|
|
from services.human_input_v2.im_channel_service import IMChannelView, WorkspaceIMChannelService
|
|
from services.human_input_v2.im_channel_service_composition import (
|
|
build_workspace_im_channel_service,
|
|
)
|
|
|
|
from ._common import StrictModel
|
|
from ._decorators import require_admin_or_owner, workspace_channel_api_admission
|
|
from .config_version import (
|
|
InvalidConfigVersionError,
|
|
decode_email_config_version,
|
|
decode_im_config_version,
|
|
encode_email_config_version,
|
|
encode_im_config_version,
|
|
)
|
|
from .errors import (
|
|
ChannelAlreadyConfiguredHttpError,
|
|
ChannelNotFoundHttpError,
|
|
ChannelProviderBadRequestHttpError,
|
|
ChannelProviderConfigurationUpdatedHttpError,
|
|
ChannelReplacementRequiredHttpError,
|
|
)
|
|
from .provider_updates import EmailProviderCredentialsUpdateInput, IMProviderCredentialsUpdateInput, UnchangedInput
|
|
from .provider_views import (
|
|
IMProviderCredentialsResponse,
|
|
Redacted,
|
|
ResendCredentialsResponse,
|
|
im_credentials_response,
|
|
)
|
|
from .providers import EmailProviderCredentials, IMProviderCredentialsInput
|
|
|
|
type ChannelId = Annotated[str, StringConstraints(strip_whitespace=True, min_length=1)]
|
|
type ConfigVersion = Annotated[str, StringConstraints(strip_whitespace=True, min_length=1)]
|
|
|
|
|
|
class WorkspaceChannelResource(Resource):
|
|
"""Apply the edition gate outside every canonical handler decorator stack."""
|
|
|
|
method_decorators = [workspace_channel_api_admission]
|
|
|
|
|
|
class ChannelStatus(StrEnum):
|
|
CONFIGURED = "configured"
|
|
CONNECTED = "connected"
|
|
INVALID_CREDENTIALS = "invalid_credentials"
|
|
CONNECTION_FAILURE = "connection_failure"
|
|
|
|
|
|
class ConnectionMode(StrEnum):
|
|
CUSTOM_APP = "custom_app"
|
|
|
|
|
|
class ChannelTestResponse(ResponseModel):
|
|
status: ChannelStatus
|
|
status_description: str
|
|
|
|
|
|
class ChannelDeleteResponse(ResponseModel):
|
|
channel_id: ChannelId
|
|
|
|
|
|
class ChannelConflictResponse(ResponseModel):
|
|
code: Literal["replacement_required", "provider_configuration_updated"]
|
|
message: str
|
|
status: Literal[HTTPStatus.CONFLICT] = HTTPStatus.CONFLICT
|
|
|
|
|
|
class ChannelProvider(ResponseModel):
|
|
provider: EmailProviderType | IMProvider
|
|
connection_mode: Literal[ConnectionMode.CUSTOM_APP] = ConnectionMode.CUSTOM_APP
|
|
|
|
|
|
class ListChannelProvidersResponse(ResponseModel):
|
|
email_providers: Sequence[ChannelProvider]
|
|
im_providers: Sequence[ChannelProvider]
|
|
|
|
|
|
class ChannelSummary(ResponseModel):
|
|
id: ChannelId
|
|
created_at: Timestamp
|
|
updated_at: Timestamp
|
|
kind: Literal[ChannelKind.EMAIL, ChannelKind.IM]
|
|
provider: EmailProviderType | IMProvider = Field(
|
|
description="The provider of the Channel. The concrete provider type depends on the `kind` field."
|
|
)
|
|
status: ChannelStatus
|
|
status_description: str = Field(
|
|
description="Human-readable status description. Empty when the status is `configured` or `connected`."
|
|
)
|
|
display_identifier: str = Field(description="The display identifier of the Channel.")
|
|
webhook_url: str | None = Field(
|
|
description=(
|
|
"Webhook URL to configure on the provider side. None when webhook configuration is not required "
|
|
"or the provider does not support webhooks."
|
|
)
|
|
)
|
|
config_version: ConfigVersion = Field(
|
|
description="The current opaque configuration version used for optimistic concurrency control."
|
|
)
|
|
|
|
|
|
class ListChannelsResponse(ResponseModel):
|
|
channels: Sequence[ChannelSummary]
|
|
|
|
|
|
type _SenderName = Annotated[str, StringConstraints(strip_whitespace=True, min_length=1, max_length=255)]
|
|
|
|
|
|
class _EmailChannelCandidatePayload(StrictModel):
|
|
sender_email: EmailStr
|
|
sender_name: _SenderName
|
|
credentials: EmailProviderCredentials
|
|
|
|
def to_owner_candidate(self) -> ResendCandidate:
|
|
return ResendCandidate(
|
|
sender_email=NormalizedEmail(self.sender_email),
|
|
sender_name=self.sender_name,
|
|
credentials=self.credentials.to_owner_credentials(),
|
|
)
|
|
|
|
|
|
class EmailChannelTestPayload(_EmailChannelCandidatePayload):
|
|
pass
|
|
|
|
|
|
class EmailChannelCreatePayload(_EmailChannelCandidatePayload):
|
|
pass
|
|
|
|
|
|
class EmailChannelMutationResponse(ResponseModel):
|
|
summary: ChannelSummary
|
|
|
|
|
|
class EmailChannelDetailResponse(ResponseModel):
|
|
summary: ChannelSummary
|
|
sender_name: str
|
|
sender_email: str
|
|
credentials: ResendCredentialsResponse
|
|
|
|
|
|
class EmailChannelUpdatePayload(StrictModel):
|
|
sender_email: EmailStr | UnchangedInput
|
|
sender_name: _SenderName | UnchangedInput
|
|
credentials: EmailProviderCredentialsUpdateInput
|
|
expected_config_version: ConfigVersion
|
|
|
|
def to_owner_update(self) -> ResendChannelUpdate:
|
|
return ResendChannelUpdate(
|
|
sender_email=(
|
|
Unchanged.VALUE if isinstance(self.sender_email, UnchangedInput) else NormalizedEmail(self.sender_email)
|
|
),
|
|
sender_name=Unchanged.VALUE if isinstance(self.sender_name, UnchangedInput) else self.sender_name,
|
|
credentials=self.credentials.to_owner_update(),
|
|
)
|
|
|
|
|
|
class ChannelDeleteQuery(StrictModel):
|
|
expected_config_version: ConfigVersion
|
|
|
|
|
|
class IMChannelTestPayload(StrictModel):
|
|
credentials: IMProviderCredentialsInput
|
|
|
|
|
|
class IMChannelCreatePayload(StrictModel):
|
|
credentials: IMProviderCredentialsInput
|
|
|
|
|
|
class IMChannelMutationResponse(ResponseModel):
|
|
summary: ChannelSummary
|
|
|
|
|
|
class IMChannelDetailResponse(ResponseModel):
|
|
summary: ChannelSummary
|
|
credentials: IMProviderCredentialsResponse
|
|
|
|
|
|
class IMChannelUpdatePayload(StrictModel):
|
|
credentials: IMProviderCredentialsUpdateInput
|
|
expected_config_version: ConfigVersion
|
|
|
|
|
|
class IMChannelReplacementPayload(StrictModel):
|
|
credentials: IMProviderCredentialsInput
|
|
expected_config_version: ConfigVersion
|
|
|
|
|
|
register_schema_models(
|
|
console_ns,
|
|
ChannelDeleteQuery,
|
|
EmailChannelTestPayload,
|
|
EmailChannelCreatePayload,
|
|
EmailChannelUpdatePayload,
|
|
IMChannelTestPayload,
|
|
IMChannelCreatePayload,
|
|
IMChannelUpdatePayload,
|
|
IMChannelReplacementPayload,
|
|
)
|
|
register_response_schema_models(
|
|
console_ns,
|
|
ChannelTestResponse,
|
|
ChannelDeleteResponse,
|
|
ChannelConflictResponse,
|
|
ChannelProvider,
|
|
ListChannelProvidersResponse,
|
|
ChannelSummary,
|
|
ListChannelsResponse,
|
|
EmailChannelMutationResponse,
|
|
EmailChannelDetailResponse,
|
|
IMChannelMutationResponse,
|
|
IMChannelDetailResponse,
|
|
)
|
|
|
|
|
|
def _workspace_scope(tenant_id: str) -> WorkspaceScope:
|
|
return WorkspaceScope(id=TenantId(tenant_id))
|
|
|
|
|
|
def _actor_id(account: Account) -> AccountId:
|
|
return AccountId(account.id)
|
|
|
|
|
|
def _channel_provider_response(provider: EmailProviderType | IMProvider) -> ChannelProvider:
|
|
return ChannelProvider(provider=provider)
|
|
|
|
|
|
def _email_channel_summary_response(view: EmailChannelView) -> ChannelSummary:
|
|
return ChannelSummary(
|
|
id=view.id,
|
|
created_at=view.created_at,
|
|
updated_at=view.updated_at,
|
|
kind=ChannelKind.EMAIL,
|
|
provider=view.provider,
|
|
status=ChannelStatus.CONFIGURED,
|
|
status_description="",
|
|
display_identifier=" ".join(part for part in (view.sender_name, view.sender_email) if part),
|
|
webhook_url=None,
|
|
config_version=encode_email_config_version(view.revision),
|
|
)
|
|
|
|
|
|
def _im_channel_summary_response(view: IMChannelView) -> ChannelSummary:
|
|
status, status_description = _im_channel_status(view)
|
|
return ChannelSummary(
|
|
id=view.id,
|
|
created_at=view.created_at,
|
|
updated_at=view.updated_at,
|
|
kind=ChannelKind.IM,
|
|
provider=view.provider,
|
|
status=status,
|
|
status_description=status_description,
|
|
display_identifier=view.app_identifier,
|
|
webhook_url=view.webhook_url,
|
|
config_version=encode_im_config_version(view.id, view.config_version),
|
|
)
|
|
|
|
|
|
def _im_channel_status(view: IMChannelView) -> tuple[ChannelStatus, str]:
|
|
if view.status is IMChannelStatus.CONNECTED:
|
|
return ChannelStatus.CONNECTED, ""
|
|
if view.status is IMChannelStatus.INVALID_CREDENTIALS:
|
|
return (
|
|
ChannelStatus.INVALID_CREDENTIALS,
|
|
view.status_reason or "The configured credentials are no longer accepted.",
|
|
)
|
|
return (
|
|
ChannelStatus.CONNECTION_FAILURE,
|
|
view.status_reason or "The configured provider connection is unavailable.",
|
|
)
|
|
|
|
|
|
def _email_revision(value: str, channel_id: EmailProviderId) -> EmailConfigurationSnapshot:
|
|
try:
|
|
return decode_email_config_version(value, channel_id)
|
|
except InvalidConfigVersionError as error:
|
|
raise ChannelProviderConfigurationUpdatedHttpError() from error
|
|
|
|
|
|
def _im_config_version(value: str, channel_id: IMChannelId) -> int:
|
|
try:
|
|
return decode_im_config_version(value, channel_id)
|
|
except InvalidConfigVersionError as error:
|
|
raise ChannelProviderConfigurationUpdatedHttpError() from error
|
|
|
|
|
|
def _workspace_im_channel_service(tenant_id: str, account: Account) -> WorkspaceIMChannelService:
|
|
return build_workspace_im_channel_service(TenantId(tenant_id), _actor_id(account))
|
|
|
|
|
|
@console_ns.route("/workspaces/current/human-input/v2/channel-providers")
|
|
class ListChannelProvidersApi(WorkspaceChannelResource):
|
|
@console_ns.response(200, "Success", console_ns.models[ListChannelProvidersResponse.__name__])
|
|
@require_admin_or_owner
|
|
@with_current_user
|
|
@with_current_tenant_id
|
|
def get(self, tenant_id: str, current_user: Account) -> ResponseReturnValue:
|
|
email_providers = build_human_input_email_channel_management_service().available_providers()
|
|
im_providers = _workspace_im_channel_service(tenant_id, current_user).available_providers()
|
|
return ListChannelProvidersResponse(
|
|
email_providers=[_channel_provider_response(provider) for provider in email_providers],
|
|
im_providers=[_channel_provider_response(provider) for provider in im_providers],
|
|
).model_dump(mode="json")
|
|
|
|
|
|
@console_ns.route("/workspaces/current/human-input/v2/channels")
|
|
class ListChannelsApi(WorkspaceChannelResource):
|
|
@console_ns.response(200, "Success", console_ns.models[ListChannelsResponse.__name__])
|
|
@require_admin_or_owner
|
|
@with_current_user
|
|
@with_current_tenant_id
|
|
def get(self, tenant_id: str, current_user: Account) -> ResponseReturnValue:
|
|
workspace_scope = _workspace_scope(tenant_id)
|
|
email = build_human_input_email_channel_management_service().get_current(workspace_scope)
|
|
im = _workspace_im_channel_service(tenant_id, current_user).get_current()
|
|
channels: list[ChannelSummary] = []
|
|
if email is not None:
|
|
channels.append(_email_channel_summary_response(email))
|
|
if im is not None:
|
|
channels.append(_im_channel_summary_response(im))
|
|
return ListChannelsResponse(channels=channels).model_dump(mode="json")
|
|
|
|
|
|
@console_ns.route("/workspaces/current/human-input/v2/channels/email/test")
|
|
class EmailChannelTestApi(WorkspaceChannelResource):
|
|
@console_ns.expect(console_ns.models[EmailChannelTestPayload.__name__])
|
|
@console_ns.response(200, "Success", console_ns.models[ChannelTestResponse.__name__])
|
|
@require_admin_or_owner
|
|
@with_current_tenant_id
|
|
def post(self, tenant_id: str) -> ResponseReturnValue:
|
|
request_body = EmailChannelTestPayload.model_validate(console_ns.payload or {})
|
|
candidate = request_body.to_owner_candidate()
|
|
try:
|
|
build_human_input_email_channel_management_service().test(
|
|
_workspace_scope(tenant_id),
|
|
candidate,
|
|
)
|
|
except ChannelProviderError as error:
|
|
return ChannelTestResponse(
|
|
status=ChannelStatus(error.kind.value),
|
|
status_description=error.status_description,
|
|
).model_dump(mode="json")
|
|
return ChannelTestResponse(
|
|
status=ChannelStatus.CONNECTED,
|
|
status_description="",
|
|
).model_dump(mode="json")
|
|
|
|
|
|
@console_ns.route("/workspaces/current/human-input/v2/channels/email/<uuid:channel_id>/test")
|
|
class EmailChannelSavedTestApi(WorkspaceChannelResource):
|
|
@console_ns.expect(console_ns.models[EmailChannelUpdatePayload.__name__])
|
|
@console_ns.response(200, "Success", console_ns.models[ChannelTestResponse.__name__])
|
|
@console_ns.response(409, "Configuration conflict", console_ns.models[ChannelConflictResponse.__name__])
|
|
@require_admin_or_owner
|
|
@with_current_tenant_id
|
|
def post(self, tenant_id: str, channel_id: UUID) -> ResponseReturnValue:
|
|
request_body = EmailChannelUpdatePayload.model_validate(console_ns.payload or {})
|
|
addressed_id = EmailProviderId(str(channel_id))
|
|
try:
|
|
build_human_input_email_channel_management_service().test_saved(
|
|
_workspace_scope(tenant_id),
|
|
addressed_id,
|
|
_email_revision(request_body.expected_config_version, addressed_id),
|
|
request_body.to_owner_update(),
|
|
)
|
|
except ChannelNotFoundError as error:
|
|
raise ChannelNotFoundHttpError() from error
|
|
except ProviderConfigurationUpdatedError as error:
|
|
raise ChannelProviderConfigurationUpdatedHttpError() from error
|
|
except ChannelProviderError as error:
|
|
return ChannelTestResponse(
|
|
status=ChannelStatus(error.kind.value),
|
|
status_description=error.status_description,
|
|
).model_dump(mode="json")
|
|
return ChannelTestResponse(status=ChannelStatus.CONNECTED, status_description="").model_dump(mode="json")
|
|
|
|
|
|
@console_ns.route("/workspaces/current/human-input/v2/channels/email")
|
|
class EmailChannelCreateApi(WorkspaceChannelResource):
|
|
@console_ns.expect(console_ns.models[EmailChannelCreatePayload.__name__])
|
|
@console_ns.response(200, "Success", console_ns.models[EmailChannelMutationResponse.__name__])
|
|
@require_admin_or_owner
|
|
@with_current_user
|
|
@with_current_tenant_id
|
|
def post(self, tenant_id: str, current_user: Account) -> ResponseReturnValue:
|
|
request_body = EmailChannelCreatePayload.model_validate(console_ns.payload or {})
|
|
try:
|
|
snapshot = build_human_input_email_channel_management_service().create(
|
|
_workspace_scope(tenant_id),
|
|
_actor_id(current_user),
|
|
request_body.to_owner_candidate(),
|
|
)
|
|
except ChannelAlreadyConfiguredError as error:
|
|
raise ChannelAlreadyConfiguredHttpError() from error
|
|
except ChannelProviderError as error:
|
|
raise ChannelProviderBadRequestHttpError(error.status_description) from error
|
|
return EmailChannelMutationResponse(summary=_email_channel_summary_response(snapshot)).model_dump(mode="json")
|
|
|
|
|
|
@console_ns.route("/workspaces/current/human-input/v2/channels/email/<uuid:channel_id>")
|
|
class EmailChannelApi(WorkspaceChannelResource):
|
|
@console_ns.response(200, "Success", console_ns.models[EmailChannelDetailResponse.__name__])
|
|
@require_admin_or_owner
|
|
@with_current_tenant_id
|
|
def get(self, tenant_id: str, channel_id: UUID) -> ResponseReturnValue:
|
|
try:
|
|
snapshot = build_human_input_email_channel_management_service().get(
|
|
_workspace_scope(tenant_id),
|
|
EmailProviderId(str(channel_id)),
|
|
)
|
|
except ChannelNotFoundError as error:
|
|
raise ChannelNotFoundHttpError() from error
|
|
return EmailChannelDetailResponse(
|
|
summary=_email_channel_summary_response(snapshot),
|
|
sender_name=snapshot.sender_name,
|
|
sender_email=snapshot.sender_email,
|
|
credentials=ResendCredentialsResponse(provider=EmailProviderType.RESEND, api_key=Redacted()),
|
|
).model_dump(mode="json")
|
|
|
|
@console_ns.expect(console_ns.models[EmailChannelUpdatePayload.__name__])
|
|
@console_ns.response(200, "Success", console_ns.models[EmailChannelMutationResponse.__name__])
|
|
@console_ns.response(409, "Configuration conflict", console_ns.models[ChannelConflictResponse.__name__])
|
|
@require_admin_or_owner
|
|
@with_current_user
|
|
@with_current_tenant_id
|
|
def put(self, tenant_id: str, current_user: Account, channel_id: UUID) -> ResponseReturnValue:
|
|
request_body = EmailChannelUpdatePayload.model_validate(console_ns.payload or {})
|
|
addressed_id = EmailProviderId(str(channel_id))
|
|
try:
|
|
snapshot = build_human_input_email_channel_management_service().update(
|
|
_workspace_scope(tenant_id),
|
|
addressed_id,
|
|
_email_revision(request_body.expected_config_version, addressed_id),
|
|
_actor_id(current_user),
|
|
request_body.to_owner_update(),
|
|
)
|
|
except ChannelNotFoundError as error:
|
|
raise ChannelNotFoundHttpError() from error
|
|
except ProviderConfigurationUpdatedError as error:
|
|
raise ChannelProviderConfigurationUpdatedHttpError() from error
|
|
except ChannelProviderError as error:
|
|
raise ChannelProviderBadRequestHttpError(error.status_description) from error
|
|
return EmailChannelMutationResponse(summary=_email_channel_summary_response(snapshot)).model_dump(mode="json")
|
|
|
|
@console_ns.doc(params=query_params_from_model(ChannelDeleteQuery))
|
|
@console_ns.response(200, "Success", console_ns.models[ChannelDeleteResponse.__name__])
|
|
@console_ns.response(409, "Configuration conflict", console_ns.models[ChannelConflictResponse.__name__])
|
|
@require_admin_or_owner
|
|
@with_current_tenant_id
|
|
def delete(self, tenant_id: str, channel_id: UUID) -> ResponseReturnValue:
|
|
query = ChannelDeleteQuery.model_validate(request.args.to_dict(flat=True))
|
|
addressed_id = EmailProviderId(str(channel_id))
|
|
try:
|
|
deleted_id = build_human_input_email_channel_management_service().delete(
|
|
_workspace_scope(tenant_id),
|
|
addressed_id,
|
|
_email_revision(query.expected_config_version, addressed_id),
|
|
)
|
|
except ChannelNotFoundError as error:
|
|
raise ChannelNotFoundHttpError() from error
|
|
except ProviderConfigurationUpdatedError as error:
|
|
raise ChannelProviderConfigurationUpdatedHttpError() from error
|
|
return ChannelDeleteResponse(channel_id=deleted_id).model_dump(mode="json")
|
|
|
|
|
|
@console_ns.route("/workspaces/current/human-input/v2/channels/im/test")
|
|
class IMChannelTestApi(WorkspaceChannelResource):
|
|
@console_ns.expect(console_ns.models[IMChannelTestPayload.__name__])
|
|
@console_ns.response(200, "Success", console_ns.models[ChannelTestResponse.__name__])
|
|
@require_admin_or_owner
|
|
@with_current_user
|
|
@with_current_tenant_id
|
|
def post(self, tenant_id: str, current_user: Account) -> ResponseReturnValue:
|
|
request_body = IMChannelTestPayload.model_validate(console_ns.payload or {})
|
|
try:
|
|
_workspace_im_channel_service(tenant_id, current_user).test(request_body.credentials.to_owner_credentials())
|
|
except ChannelProviderError as error:
|
|
return ChannelTestResponse(
|
|
status=ChannelStatus(error.kind.value),
|
|
status_description=error.status_description,
|
|
).model_dump(mode="json")
|
|
return ChannelTestResponse(
|
|
status=ChannelStatus.CONNECTED,
|
|
status_description="",
|
|
).model_dump(mode="json")
|
|
|
|
|
|
@console_ns.route("/workspaces/current/human-input/v2/channels/im/<uuid:channel_id>/test")
|
|
class IMChannelSavedTestApi(WorkspaceChannelResource):
|
|
@console_ns.expect(console_ns.models[IMChannelUpdatePayload.__name__])
|
|
@console_ns.response(200, "Success", console_ns.models[ChannelTestResponse.__name__])
|
|
@console_ns.response(400, "The test provider must match the saved IM Channel.")
|
|
@console_ns.response(409, "Configuration conflict", console_ns.models[ChannelConflictResponse.__name__])
|
|
@require_admin_or_owner
|
|
@with_current_user
|
|
@with_current_tenant_id
|
|
def post(self, tenant_id: str, current_user: Account, channel_id: UUID) -> ResponseReturnValue:
|
|
request_body = IMChannelUpdatePayload.model_validate(console_ns.payload or {})
|
|
addressed_id = IMChannelId(str(channel_id))
|
|
try:
|
|
_workspace_im_channel_service(tenant_id, current_user).test_saved(
|
|
addressed_id,
|
|
_im_config_version(request_body.expected_config_version, addressed_id),
|
|
request_body.credentials.to_owner_update(),
|
|
)
|
|
except ChannelNotFoundError as error:
|
|
raise ChannelNotFoundHttpError() from error
|
|
except ProviderConfigurationUpdatedError as error:
|
|
raise ChannelProviderConfigurationUpdatedHttpError() from error
|
|
except ReplacementRequiredError as error:
|
|
raise ChannelProviderBadRequestHttpError("The test provider must match the saved IM Channel.") from error
|
|
except ChannelProviderError as error:
|
|
return ChannelTestResponse(
|
|
status=ChannelStatus(error.kind.value),
|
|
status_description=error.status_description,
|
|
).model_dump(mode="json")
|
|
return ChannelTestResponse(status=ChannelStatus.CONNECTED, status_description="").model_dump(mode="json")
|
|
|
|
|
|
@console_ns.route("/workspaces/current/human-input/v2/channels/im")
|
|
class IMChannelCreateApi(WorkspaceChannelResource):
|
|
@console_ns.expect(console_ns.models[IMChannelCreatePayload.__name__])
|
|
@console_ns.response(200, "Success", console_ns.models[IMChannelMutationResponse.__name__])
|
|
@require_admin_or_owner
|
|
@with_current_user
|
|
@with_current_tenant_id
|
|
def post(self, tenant_id: str, current_user: Account) -> ResponseReturnValue:
|
|
request_body = IMChannelCreatePayload.model_validate(console_ns.payload or {})
|
|
try:
|
|
snapshot = _workspace_im_channel_service(tenant_id, current_user).create(
|
|
request_body.credentials.to_owner_credentials()
|
|
)
|
|
except ChannelAlreadyConfiguredError as error:
|
|
raise ChannelAlreadyConfiguredHttpError() from error
|
|
except ChannelProviderError as error:
|
|
raise ChannelProviderBadRequestHttpError(error.status_description) from error
|
|
return IMChannelMutationResponse(summary=_im_channel_summary_response(snapshot)).model_dump(mode="json")
|
|
|
|
|
|
@console_ns.route("/workspaces/current/human-input/v2/channels/im/<uuid:channel_id>")
|
|
class IMChannelApi(WorkspaceChannelResource):
|
|
@console_ns.response(200, "Success", console_ns.models[IMChannelDetailResponse.__name__])
|
|
@require_admin_or_owner
|
|
@with_current_user
|
|
@with_current_tenant_id
|
|
def get(self, tenant_id: str, current_user: Account, channel_id: UUID) -> ResponseReturnValue:
|
|
try:
|
|
detail = _workspace_im_channel_service(tenant_id, current_user).get_detail(IMChannelId(str(channel_id)))
|
|
except ChannelNotFoundError as error:
|
|
raise ChannelNotFoundHttpError() from error
|
|
return IMChannelDetailResponse(
|
|
summary=_im_channel_summary_response(detail.summary),
|
|
credentials=im_credentials_response(detail.credentials),
|
|
).model_dump(mode="json")
|
|
|
|
@console_ns.expect(console_ns.models[IMChannelUpdatePayload.__name__])
|
|
@console_ns.response(200, "Success", console_ns.models[IMChannelMutationResponse.__name__])
|
|
@console_ns.response(409, "Configuration conflict", console_ns.models[ChannelConflictResponse.__name__])
|
|
@require_admin_or_owner
|
|
@with_current_user
|
|
@with_current_tenant_id
|
|
def put(self, tenant_id: str, current_user: Account, channel_id: UUID) -> ResponseReturnValue:
|
|
request_body = IMChannelUpdatePayload.model_validate(console_ns.payload or {})
|
|
addressed_id = IMChannelId(str(channel_id))
|
|
try:
|
|
snapshot = _workspace_im_channel_service(tenant_id, current_user).update(
|
|
addressed_id,
|
|
_im_config_version(request_body.expected_config_version, addressed_id),
|
|
request_body.credentials.to_owner_update(),
|
|
)
|
|
except ChannelNotFoundError as error:
|
|
raise ChannelNotFoundHttpError() from error
|
|
except ReplacementRequiredError as error:
|
|
raise ChannelReplacementRequiredHttpError() from error
|
|
except ProviderConfigurationUpdatedError as error:
|
|
raise ChannelProviderConfigurationUpdatedHttpError() from error
|
|
except ChannelProviderError as error:
|
|
raise ChannelProviderBadRequestHttpError(error.status_description) from error
|
|
return IMChannelMutationResponse(summary=_im_channel_summary_response(snapshot)).model_dump(mode="json")
|
|
|
|
@console_ns.doc(params=query_params_from_model(ChannelDeleteQuery))
|
|
@console_ns.response(200, "Success", console_ns.models[ChannelDeleteResponse.__name__])
|
|
@console_ns.response(409, "Configuration conflict", console_ns.models[ChannelConflictResponse.__name__])
|
|
@require_admin_or_owner
|
|
@with_current_user
|
|
@with_current_tenant_id
|
|
def delete(self, tenant_id: str, current_user: Account, channel_id: UUID) -> ResponseReturnValue:
|
|
query = ChannelDeleteQuery.model_validate(request.args.to_dict(flat=True))
|
|
addressed_id = IMChannelId(str(channel_id))
|
|
try:
|
|
deleted_id = _workspace_im_channel_service(tenant_id, current_user).delete(
|
|
addressed_id,
|
|
_im_config_version(query.expected_config_version, addressed_id),
|
|
)
|
|
except ChannelNotFoundError as error:
|
|
raise ChannelNotFoundHttpError() from error
|
|
except ProviderConfigurationUpdatedError as error:
|
|
raise ChannelProviderConfigurationUpdatedHttpError() from error
|
|
return ChannelDeleteResponse(channel_id=deleted_id).model_dump(mode="json")
|
|
|
|
|
|
@console_ns.route("/workspaces/current/human-input/v2/channels/im/<uuid:channel_id>/replacement")
|
|
class IMChannelReplacementApi(WorkspaceChannelResource):
|
|
@console_ns.expect(console_ns.models[IMChannelReplacementPayload.__name__])
|
|
@console_ns.response(200, "Success", console_ns.models[IMChannelMutationResponse.__name__])
|
|
@console_ns.response(409, "Configuration conflict", console_ns.models[ChannelConflictResponse.__name__])
|
|
@require_admin_or_owner
|
|
@with_current_user
|
|
@with_current_tenant_id
|
|
def post(self, tenant_id: str, current_user: Account, channel_id: UUID) -> ResponseReturnValue:
|
|
request_body = IMChannelReplacementPayload.model_validate(console_ns.payload or {})
|
|
addressed_id = IMChannelId(str(channel_id))
|
|
try:
|
|
snapshot = _workspace_im_channel_service(tenant_id, current_user).replace(
|
|
addressed_id,
|
|
_im_config_version(request_body.expected_config_version, addressed_id),
|
|
request_body.credentials.to_owner_credentials(),
|
|
)
|
|
except ChannelNotFoundError as error:
|
|
raise ChannelNotFoundHttpError() from error
|
|
except ProviderConfigurationUpdatedError as error:
|
|
raise ChannelProviderConfigurationUpdatedHttpError() from error
|
|
except ChannelProviderError as error:
|
|
raise ChannelProviderBadRequestHttpError(error.status_description) from error
|
|
return IMChannelMutationResponse(summary=_im_channel_summary_response(snapshot)).model_dump(mode="json")
|
|
|
|
|
|
__all__ = [
|
|
"ChannelConflictResponse",
|
|
"ChannelDeleteQuery",
|
|
"ChannelDeleteResponse",
|
|
"ChannelProvider",
|
|
"ChannelSummary",
|
|
"ChannelTestResponse",
|
|
"EmailChannelCreatePayload",
|
|
"EmailChannelDetailResponse",
|
|
"EmailChannelTestPayload",
|
|
"EmailChannelUpdatePayload",
|
|
"IMChannelCreatePayload",
|
|
"IMChannelDetailResponse",
|
|
"IMChannelReplacementPayload",
|
|
"IMChannelTestPayload",
|
|
"IMChannelUpdatePayload",
|
|
"ListChannelProvidersResponse",
|
|
"ListChannelsResponse",
|
|
]
|