feat(models): 组织、用户、角色、客户、会话与消息模型
按组织边界定义数据关系,补齐仓储层与通用工具,迁移脚本可重复执行。
This commit is contained in:
1 parent
977783c126
commit
9eb8613c05
126 files changed
+17436
No files matched your search
@@ -0,0 +1,229 @@
|
||||
"""
|
||||
Copyright 2024-2026 ReplyPier
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
"""
|
||||
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
from uuid import UUID
|
||||
|
||||
from pydantic import BaseModel, field_validator
|
||||
|
||||
from app.channels.constants import DEFAULT_TEMPLATE_LANGUAGE
|
||||
|
||||
|
||||
class ChannelAccountOut(BaseModel):
|
||||
"""Connected channel account as exposed to the settings UI (no secrets)."""
|
||||
id: UUID
|
||||
channel_type: str
|
||||
external_account_id: str
|
||||
display_name: Optional[str] = None
|
||||
is_active: bool
|
||||
agent_id: Optional[UUID] = None # agent currently routed to this account
|
||||
created_at: Optional[datetime] = None
|
||||
# For channels the customer must point at us (email inbound-parse, SMS,
|
||||
# Meta accounts from the customer's own app): the exact webhook URL to
|
||||
# configure on their provider, and the verify token Meta asks for.
|
||||
webhook_url: Optional[str] = None
|
||||
# The platform refused the account's credentials; replies fail until the
|
||||
# account is reconnected
|
||||
needs_reconnect: bool = False
|
||||
webhook_verify_token: Optional[str] = None
|
||||
# WhatsApp from the customer's own app: whether Meta took our per-number
|
||||
# callback (else the customer sets the URL and token by hand)
|
||||
webhook_auto_configured: Optional[bool] = None
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class TelegramConnectRequest(BaseModel):
|
||||
bot_token: str
|
||||
|
||||
|
||||
class OwnMetaAppSecret(BaseModel):
|
||||
"""The secret of the customer's own Meta app, for manual credentials:
|
||||
Meta signs that app's webhooks with it. Blank means not given."""
|
||||
app_secret: Optional[str] = None
|
||||
|
||||
@field_validator("app_secret")
|
||||
@classmethod
|
||||
def _blank_is_none(cls, value: Optional[str]) -> Optional[str]:
|
||||
return (value or "").strip() or None
|
||||
|
||||
|
||||
class WhatsAppConnectRequest(OwnMetaAppSecret):
|
||||
"""Manual WhatsApp Cloud API credentials, from the customer's own Meta app."""
|
||||
phone_number_id: str
|
||||
access_token: str
|
||||
waba_id: Optional[str] = None
|
||||
display_name: Optional[str] = None
|
||||
|
||||
|
||||
class MessengerConnectRequest(OwnMetaAppSecret):
|
||||
page_id: str
|
||||
page_access_token: str
|
||||
display_name: Optional[str] = None
|
||||
|
||||
|
||||
class InstagramConnectRequest(BaseModel):
|
||||
ig_id: str
|
||||
page_access_token: str
|
||||
display_name: Optional[str] = None
|
||||
|
||||
|
||||
class TemplateSendRequest(BaseModel):
|
||||
"""Reopen an expired WhatsApp window with an approved template."""
|
||||
session_id: UUID
|
||||
template_name: str
|
||||
language: str = DEFAULT_TEMPLATE_LANGUAGE
|
||||
components: Optional[list] = None
|
||||
|
||||
|
||||
class OutboundConversationRequest(BaseModel):
|
||||
"""Start a WhatsApp conversation with a phone number — the customer has
|
||||
not messaged us; an approved template is the only thing Meta will deliver.
|
||||
|
||||
customer_id links the conversation to a person the agent picked in People;
|
||||
without it the customer is resolved by phone or created. customer_name
|
||||
only names a newly created person (never renames an existing one)."""
|
||||
to: str
|
||||
template_name: str
|
||||
language: str = DEFAULT_TEMPLATE_LANGUAGE
|
||||
components: Optional[list] = None
|
||||
customer_id: Optional[UUID] = None
|
||||
customer_name: Optional[str] = None
|
||||
|
||||
|
||||
class OutboundConversationOut(BaseModel):
|
||||
"""The session the outbound send created (or reused): everything else —
|
||||
the inbox thread, template resend, AI takeover on reply — keys off it."""
|
||||
session_id: UUID
|
||||
|
||||
|
||||
class EmbeddedSignupRequest(BaseModel):
|
||||
"""What the Embedded Signup JS SDK hands back: a short-lived code to trade
|
||||
for the customer's business token, plus the assets it created."""
|
||||
code: str
|
||||
waba_id: str
|
||||
phone_number_id: str
|
||||
display_name: Optional[str] = None
|
||||
|
||||
|
||||
class MessengerSignupRequest(BaseModel):
|
||||
"""The code Facebook Login for Business hands back. Unlike WhatsApp's
|
||||
Embedded Signup it names no assets — the Pages are ours to look up.
|
||||
|
||||
redirect_uri is the OAuth callback URL the login popup used; the code is
|
||||
bound to it, so the token exchange must send the exact same value back. The
|
||||
client owns it (it must also be a registered Valid OAuth Redirect URI)."""
|
||||
code: str
|
||||
redirect_uri: str
|
||||
|
||||
|
||||
class InstagramLoginRequest(BaseModel):
|
||||
"""What Instagram Business Login hands back. No Page and no asset ids — the
|
||||
token itself identifies the single account that signed in, so unlike the
|
||||
Messenger flow there is nothing to list or pick.
|
||||
|
||||
redirect_uri is the OAuth callback the popup used; the code is bound to it,
|
||||
so the token exchange must send the exact same value back."""
|
||||
code: str
|
||||
redirect_uri: str
|
||||
display_name: Optional[str] = None
|
||||
|
||||
|
||||
class MessengerPageOut(BaseModel):
|
||||
"""A Page offered in the picker. Its access token is deliberately absent:
|
||||
it travels back inside the opaque signup_token instead."""
|
||||
id: str
|
||||
name: str
|
||||
|
||||
|
||||
class MessengerSignupPagesOut(BaseModel):
|
||||
"""Step 1's result: the Pages to choose from, plus an opaque token that
|
||||
carries their access tokens (encrypted) into step 2."""
|
||||
pages: list[MessengerPageOut]
|
||||
signup_token: str
|
||||
|
||||
|
||||
class MessengerSignupConnectRequest(BaseModel):
|
||||
"""Step 2: the Page picked out of step 1's list. page_id is a selector into
|
||||
that list, not a credential — the token is the one we sealed."""
|
||||
signup_token: str
|
||||
page_id: str
|
||||
display_name: Optional[str] = None
|
||||
|
||||
|
||||
class EmbeddedSignupConfigOut(BaseModel):
|
||||
"""Tells the connect UI whether to offer Embedded Signup. Everything but
|
||||
`enabled` is None when it isn't, so nothing leaks to orgs without it."""
|
||||
enabled: bool
|
||||
config_id: Optional[str] = None
|
||||
app_id: Optional[str] = None
|
||||
graph_version: str
|
||||
|
||||
|
||||
class TemplateOut(BaseModel):
|
||||
"""A message template as it exists on the customer's WhatsApp Business
|
||||
Account. Only APPROVED templates can actually be sent."""
|
||||
id: Optional[str] = None
|
||||
name: str
|
||||
status: Optional[str] = None
|
||||
category: Optional[str] = None
|
||||
language: Optional[str] = None
|
||||
components: Optional[list] = None
|
||||
|
||||
|
||||
class TemplateLibraryOut(BaseModel):
|
||||
"""A deep link into Meta's Template Library for one WhatsApp Business
|
||||
Account — which is where templates get written. We list and send them; Meta
|
||||
authors them."""
|
||||
url: str
|
||||
|
||||
|
||||
class AgentChannelConfigRequest(BaseModel):
|
||||
agent_id: UUID
|
||||
is_active: bool = True
|
||||
|
||||
|
||||
class EmailConnectRequest(BaseModel):
|
||||
inbound_address: str # e.g. support@acme.com (the address customers write to)
|
||||
display_name: Optional[str] = None
|
||||
# Optional per-inbox outbound SMTP. When omitted, replies use the platform
|
||||
# SMTP settings. Provide these to send from the inbox's own domain with
|
||||
# correct SPF/DKIM alignment.
|
||||
smtp_host: Optional[str] = None
|
||||
smtp_port: Optional[int] = None
|
||||
smtp_username: Optional[str] = None
|
||||
smtp_password: Optional[str] = None
|
||||
from_email: Optional[str] = None # defaults to inbound_address
|
||||
smtp_use_ssl: Optional[bool] = None # None = auto (port 465 → implicit TLS)
|
||||
|
||||
|
||||
class SmsConnectRequest(BaseModel):
|
||||
provider: str # 'twilio' | 'vonage' | 'messagebird' | ...
|
||||
phone_number: str # sender number / id
|
||||
credentials: dict = {} # provider-specific secrets
|
||||
|
||||
|
||||
class SmsProviderInfo(BaseModel):
|
||||
name: str
|
||||
label: str
|
||||
fields: list # [{key,label,secret,optional}] for the UI
|
||||
|
||||
|
||||
class LineConnectRequest(BaseModel):
|
||||
channel_secret: str
|
||||
channel_access_token: str
|
||||
Reference in new issue
Block a user