One word everywhere: ReplyPier in UI copy, package and image names, the replypier.site domains (app/api/docs/help), REPLYPIER_* settings and the --rp-* design tokens. - public widget API is window.ReplyPier with replypierId / replypierBaseUrl / replypierConfig / replypierToken, loader at webclient/replypier.min.js; both bundles rebuilt from source - NOTICE, llms.txt, CONTRIBUTING, HELP_CENTER_INFRA and the demo runtime config rewritten; the demo config ships no third-party keys - migration adds the ReplyPier managed model value to the AI model enum - local compose project, database, role, volume and Docker network carry the same name
230 lines
7.7 KiB
Python
230 lines
7.7 KiB
Python
"""
|
|
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
|