feat(widget): Widget 配置、loader 与右下角悬浮入口
生成可嵌入站点的 loader 与悬浮按钮,支持展开收起和移动端;附独立宿主测试页。
This commit is contained in:
1 parent
556b650c7a
commit
0cc6299d1c
57 files changed
+32086
No files matched your search
@@ -0,0 +1,95 @@
|
||||
"""
|
||||
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.
|
||||
|
||||
What a widget conversation token may do to the customer it names.
|
||||
|
||||
A widget visitor can be bound to a customer by an email they only typed, so the
|
||||
token naming a customer is not proof of being them. Conversations are already
|
||||
fenced by the token's scope (see conversation_token.conversation_scope); this
|
||||
module fences the customer row itself - its contact details, merges, and whether
|
||||
an unverified token may stand for a person the embedding app has signed in.
|
||||
"""
|
||||
|
||||
from typing import Optional
|
||||
from uuid import UUID
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.models.customer import Customer
|
||||
from app.models.session_to_agent import SessionToAgent
|
||||
from app.repositories.customer import CustomerRepository
|
||||
from app.repositories.session_to_agent import SessionToAgentRepository
|
||||
from app.services.conversation_token import (
|
||||
LEGACY_SCOPE_PREFIX,
|
||||
is_verified_scope,
|
||||
scope_for_session_key,
|
||||
)
|
||||
|
||||
|
||||
def can_act_as(db: Session, customer: Optional[Customer], scope: Optional[str]) -> bool:
|
||||
"""True when a token with `scope` may change `customer` as if it were them.
|
||||
|
||||
A verified token always may. An unverified one only for a customer its own
|
||||
scope created - "no conversations elsewhere" is not ownership: a customer
|
||||
made by a ticket or an import has none - and only while every conversation
|
||||
of that customer is still its own.
|
||||
"""
|
||||
if customer is None or not scope:
|
||||
return False
|
||||
if is_verified_scope(scope):
|
||||
return True
|
||||
if customer.is_authenticated or not _created_by(customer, scope):
|
||||
return False
|
||||
return not SessionToAgentRepository(db).customer_has_sessions_outside_scope(customer.id, scope)
|
||||
|
||||
|
||||
def _created_by(customer: Customer, scope: str) -> bool:
|
||||
if customer.conversation_key:
|
||||
return customer.conversation_key == scope
|
||||
# Customers from before conversation keys: the anonymous row a legacy
|
||||
# widget token created for itself is the only kind such a token can own.
|
||||
return scope.startswith(LEGACY_SCOPE_PREFIX) and CustomerRepository.is_placeholder_email(customer.email)
|
||||
|
||||
|
||||
def may_stand_for(customer: Optional[Customer], scope: Optional[str]) -> bool:
|
||||
"""True when an unverified token may keep naming `customer` at all.
|
||||
|
||||
Once the embedding app has signed a person in, only a token our side vouched
|
||||
for may name them - otherwise whoever claimed the address first would keep a
|
||||
token for the signed-in user's row.
|
||||
"""
|
||||
if customer is None or not scope:
|
||||
return False
|
||||
return is_verified_scope(scope) or not customer.is_authenticated
|
||||
|
||||
|
||||
def trusts_identity_in_conversation(
|
||||
db: Session, customer: Optional[Customer], conversation: Optional[SessionToAgent]
|
||||
) -> bool:
|
||||
"""True when the visitor in `conversation` may be told, or change, what we
|
||||
know of `customer`.
|
||||
|
||||
Reciting a stored name or email back is how a visitor who only typed someone
|
||||
else's address would learn who they are. Unknown conversation: nothing.
|
||||
"""
|
||||
if conversation is None:
|
||||
return False
|
||||
return can_act_as(db, customer, scope_for_session_key(conversation.conversation_key))
|
||||
|
||||
|
||||
def trusts_identity_in_session(db: Session, customer: Optional[Customer], session_id: UUID | str | None) -> bool:
|
||||
"""trusts_identity_in_conversation, for a caller holding only the session id."""
|
||||
conversation = SessionToAgentRepository(db).get_session(session_id) if session_id else None
|
||||
return trusts_identity_in_conversation(db, customer, conversation)
|
||||
Reference in new issue
Block a user