Merge branch 'master' into ep/link-notices

This commit is contained in:
Evgeny @ SimpleX Chat committed 2026-10-20 21:46:21 +00:00
commit 2a86c24a57
1320 files changed
+121675 -7495

No files matched your search

@@ -7,6 +7,7 @@ from __future__ import annotations
import ctypes
import errno
import hashlib
import os
import platform
import sys
@@ -18,7 +19,7 @@ from ctypes import POINTER, c_char_p, c_int, c_uint8, c_void_p
from pathlib import Path
from typing import Any, Literal
from ._version import LIBS_VERSION
from ._version import LIBS_RELEASE, LIBS_SHA256
Backend = Literal["sqlite", "postgres"]
@@ -59,11 +60,14 @@ def _libname() -> str:
return _LIBNAME[sys.platform]
def _libs_url(backend: Backend) -> str:
def _libs_asset(backend: Backend) -> str:
suffix = "-postgres" if backend == "postgres" else ""
return f"simplex-chat-libs-{_platform_tag()}{suffix}.zip"
def _libs_url(backend: Backend) -> str:
return (
f"https://github.com/{_GITHUB_REPO}/releases/download/"
f"v{LIBS_VERSION}/simplex-chat-libs-{_platform_tag()}{suffix}.zip"
f"https://github.com/{_GITHUB_REPO}/releases/download/{LIBS_RELEASE}/{_libs_asset(backend)}"
)
@@ -84,7 +88,7 @@ def _resolve_libs_dir(backend: Backend) -> Path:
"postgres backend is only supported on linux-x86_64; "
f"current platform is {_platform_tag()}"
)
target = _cache_root() / f"v{LIBS_VERSION}" / backend
target = _cache_root() / LIBS_RELEASE / backend
if not (target / _libname()).exists():
_download(target, backend)
return target
@@ -125,16 +129,21 @@ def _download(target: Path, backend: Backend) -> None:
what we need and proceed. NEVER rmtree the target: that creates a TOCTOU
window where another process is reading/loading the file we're deleting.
"""
asset = _libs_asset(backend)
sha256 = LIBS_SHA256.get(asset)
if sha256 is None:
raise RuntimeError(f"no SHA-256 is pinned for {asset}")
target.parent.mkdir(parents=True, exist_ok=True)
url = _libs_url(backend)
print(
f"Downloading libsimplex ({_platform_tag()}, {backend}) v{LIBS_VERSION} from {url} ...",
f"Downloading libsimplex ({_platform_tag()}, {backend}) {LIBS_RELEASE} from {url} ...",
file=sys.stderr,
flush=True,
)
with tempfile.TemporaryDirectory(dir=target.parent) as tmp:
zip_path = Path(tmp) / "libs.zip"
_stream_to_file(url, zip_path, timeout=60.0)
_verify_sha256(zip_path, sha256, url)
with zipfile.ZipFile(zip_path) as zf:
zf.extractall(tmp)
# zip layout: <tmp>/libs/libsimplex.* + libHS*.*
@@ -158,6 +167,16 @@ def _download(target: Path, backend: Backend) -> None:
) from e
def _verify_sha256(file: Path, expected: str, url: str) -> None:
digest = hashlib.sha256()
with file.open("rb") as f:
while chunk := f.read(_DOWNLOAD_CHUNK):
digest.update(chunk)
actual = digest.hexdigest()
if actual != expected:
raise RuntimeError(f"SHA-256 of {url} is {actual}, expected {expected}")
_lock = threading.Lock()
_lib: ctypes.CDLL | None = None
_libc: ctypes.CDLL | None = None
@@ -230,8 +249,8 @@ def lib_for(backend: Backend) -> ctypes.CDLL:
if _lib is not None:
if _backend != backend:
raise RuntimeError(
f"libsimplex already loaded with backend={_backend!r}; "
f"cannot switch to {backend!r} in the same process"
f"libsimplex already loaded with backend={_backend}; "
f"cannot switch to {backend} in the same process"
)
return _lib
libs_dir = _resolve_libs_dir(backend)
@@ -1,9 +1,16 @@
"""Single source of truth for both the Python package version and the
simplex-chat-libs release tag we depend on.
"""Package version and the simplex-chat-libs release it loads.
Bump both together for normal releases. For wrapper-only fixes use a PEP 440
post-release: __version__ = "6.5.2.post1", LIBS_VERSION unchanged.
Written by scripts/ci/packages_release.py prepare: it bumps the version
and pins new library releases with the SHA-256 of their assets.
"""
__version__ = "7.1.0b4" # PEP 440 — read by hatchling for wheel metadata
LIBS_VERSION = "7.1.0-beta.4" # simplex-chat-libs release tag (no 'v' prefix)
__version__ = "7.1.0b6.post2" # PEP 440 — read by hatchling for wheel metadata
LIBS_RELEASE = "core-7.1.0.14" # simplex-chat-libs release tag
LIBS_SHA256 = {
"simplex-chat-libs-linux-aarch64.zip": "375a6021f0c2e5d79b2cd192e17c6c22c8eae73f46e3758fba24ee5c26a671aa",
"simplex-chat-libs-linux-x86_64-postgres.zip": "c3e8a54d37801e685c6361c4e7e53e1f5163d3aa9195dcfe686b6cf90e668ef3",
"simplex-chat-libs-linux-x86_64.zip": "cc41ab9049d61be3b386d799fa9394f2870e8ae3b2b9b8745dd3f53ba99f211e",
"simplex-chat-libs-macos-aarch64.zip": "ac3a46004f25976f71b3ead8a20b9a01c170fe12f125a0a6799ae97ea990f234",
"simplex-chat-libs-macos-x86_64.zip": "0d85ffcccc3a6dfe3e0da18d0b7be9ef36ad27381f047ef66cfde67ee4a7e882",
"simplex-chat-libs-windows-x86_64.zip": "f1d1c7efc333a5ec5328a5843c9d3c9ef038fb2886c0b60ac12e87a5c1e85bbd",
}
@@ -351,6 +351,23 @@ class ChatApi:
return r["member"]
raise ChatCommandError("error adding member", r)
async def api_add_member_via_group(
self, group_id: int, via_group_id: int, group_member_id: int, member_role: T.GroupMemberRole
) -> T.GroupMember:
r = await self.send_chat_cmd(
CC.APIAddMemberViaGroup_cmd_string(
{
"groupId": group_id,
"viaGroupId": via_group_id,
"groupMemberId": group_member_id,
"memberRole": member_role,
}
)
)
if r["type"] == "sentGroupInvitationViaGroup":
return r["member"]
raise ChatCommandError("error adding member", r)
async def api_join_group(self, group_id: int) -> T.GroupInfo:
r = await self.send_chat_cmd(CC.APIJoinGroup_cmd_string({"groupId": group_id}))
if r["type"] == "userAcceptedGroupSent":
@@ -235,6 +235,21 @@ def APIAddMember_cmd_string(self: APIAddMember) -> str:
APIAddMember_Response = CR.SentGroupInvitation | CR.ChatCmdError
# Add member of another group to group. Requires bot to have Admin role in the group, and direct messages allowed for its role in another group.
# Network usage: interactive.
class APIAddMemberViaGroup(TypedDict):
groupId: int # int64
viaGroupId: int # int64
groupMemberId: int # int64
memberRole: "T.GroupMemberRole"
def APIAddMemberViaGroup_cmd_string(self: APIAddMemberViaGroup) -> str:
return '/_add #' + str(self['groupId']) + ' #' + str(self['viaGroupId']) + ' ' + str(self['groupMemberId']) + ' ' + str(self['memberRole'])
APIAddMemberViaGroup_Response = CR.SentGroupInvitationViaGroup | CR.ChatCmdError
# Join group.
# Network usage: interactive.
class APIJoinGroup(TypedDict):
@@ -84,6 +84,15 @@ class ReceivedGroupInvitation(TypedDict):
fromMemberRole: "T.GroupMemberRole"
memberRole: "T.GroupMemberRole"
class ReceivedGroupInvitationViaGroup(TypedDict):
type: Literal["receivedGroupInvitationViaGroup"]
user: "T.User"
groupInfo: "T.GroupInfo"
viaGroupInfo: "T.GroupInfo"
viaMember: "T.GroupMember"
fromMemberRole: "T.GroupMemberRole"
memberRole: "T.GroupMemberRole"
class UserJoinedGroup(TypedDict):
type: Literal["userJoinedGroup"]
user: "T.User"
@@ -352,6 +361,7 @@ ChatEvent = (
| GroupChatItemsDeleted
| ChatItemsStatusesUpdated
| ReceivedGroupInvitation
| ReceivedGroupInvitationViaGroup
| UserJoinedGroup
| GroupUpdated
| JoinedGroupMember
@@ -394,7 +404,7 @@ ChatEvent = (
| ChatErrors
)
ChatEvent_Tag = Literal["contactConnected", "contactUpdated", "contactDeletedByContact", "receivedContactRequest", "newMemberContactReceivedInv", "contactSndReady", "newChatItems", "chatItemReaction", "chatItemsDeleted", "chatItemUpdated", "groupChatItemsDeleted", "chatItemsStatusesUpdated", "receivedGroupInvitation", "userJoinedGroup", "groupUpdated", "joinedGroupMember", "memberRole", "deletedMember", "leftMember", "deletedMemberUser", "groupDeleted", "connectedToGroupMember", "memberAcceptedByOther", "memberBlockedForAll", "groupMemberUpdated", "groupLinkDataUpdated", "groupRelayUpdated", "rcvFileDescrReady", "rcvFileComplete", "sndFileCompleteXFTP", "rcvFileStart", "rcvFileSndCancelled", "rcvFileAccepted", "rcvFileError", "rcvFileWarning", "sndFileError", "sndFileWarning", "acceptingContactRequest", "acceptingBusinessRequest", "contactConnecting", "businessLinkConnecting", "joinedGroupMemberConnecting", "groupLinkConnecting", "hostConnected", "hostDisconnected", "subscriptionStatus", "serviceRequest", "serviceReplySent", "remoteCtrlSessionCode", "remoteCtrlStopped", "messageError", "chatError", "chatErrors"]
ChatEvent_Tag = Literal["contactConnected", "contactUpdated", "contactDeletedByContact", "receivedContactRequest", "newMemberContactReceivedInv", "contactSndReady", "newChatItems", "chatItemReaction", "chatItemsDeleted", "chatItemUpdated", "groupChatItemsDeleted", "chatItemsStatusesUpdated", "receivedGroupInvitation", "receivedGroupInvitationViaGroup", "userJoinedGroup", "groupUpdated", "joinedGroupMember", "memberRole", "deletedMember", "leftMember", "deletedMemberUser", "groupDeleted", "connectedToGroupMember", "memberAcceptedByOther", "memberBlockedForAll", "groupMemberUpdated", "groupLinkDataUpdated", "groupRelayUpdated", "rcvFileDescrReady", "rcvFileComplete", "sndFileCompleteXFTP", "rcvFileStart", "rcvFileSndCancelled", "rcvFileAccepted", "rcvFileError", "rcvFileWarning", "sndFileError", "sndFileWarning", "acceptingContactRequest", "acceptingBusinessRequest", "contactConnecting", "businessLinkConnecting", "joinedGroupMemberConnecting", "groupLinkConnecting", "hostConnected", "hostDisconnected", "subscriptionStatus", "serviceRequest", "serviceReplySent", "remoteCtrlSessionCode", "remoteCtrlStopped", "messageError", "chatError", "chatErrors"]
class OnEventDecorator(Protocol):
@@ -483,6 +493,12 @@ class OnEventDecorator(Protocol):
Callable[["ReceivedGroupInvitation"], Awaitable[None]],
]: ...
@overload
def __call__(self, event: Literal["receivedGroupInvitationViaGroup"], /) -> Callable[
[Callable[["ReceivedGroupInvitationViaGroup"], Awaitable[None]]],
Callable[["ReceivedGroupInvitationViaGroup"], Awaitable[None]],
]: ...
@overload
def __call__(self, event: Literal["userJoinedGroup"], /) -> Callable[
[Callable[["UserJoinedGroup"], Awaitable[None]]],
@@ -264,6 +264,14 @@ class SentGroupInvitation(TypedDict):
contact: "T.Contact"
member: "T.GroupMember"
class SentGroupInvitationViaGroup(TypedDict):
type: Literal["sentGroupInvitationViaGroup"]
user: "T.User"
groupInfo: "T.GroupInfo"
member: "T.GroupMember"
viaGroupInfo: "T.GroupInfo"
viaMember: "T.GroupMember"
class SentInvitation(TypedDict):
type: Literal["sentInvitation"]
user: "T.User"
@@ -402,6 +410,7 @@ ChatResponse = (
| RemoteCtrlConnecting
| SentConfirmation
| SentGroupInvitation
| SentGroupInvitationViaGroup
| SentInvitation
| SentInvitationToContact
| ServiceReplyAccepted
@@ -420,4 +429,4 @@ ChatResponse = (
| ApiChats
)
ChatResponse_Tag = Literal["acceptingContactRequest", "activeUser", "chatItemNotChanged", "chatItemReaction", "chatItemUpdated", "chatItemsDeleted", "chatMsgContent", "chatRunning", "chatStarted", "chatStopped", "cmdOk", "chatCmdError", "connectionPlan", "contactAlreadyExists", "contactConnectionDeleted", "contactDeleted", "contactPrefsUpdated", "contactRequestRejected", "contactsList", "groupDeletedUser", "groupLink", "groupLinkCreated", "groupLinkDeleted", "groupCreated", "publicGroupCreated", "publicGroupCreationFailed", "groupRelays", "groupRelaysAdded", "groupRelaysAddFailed", "relayGroupAllowed", "groupMembers", "groupUpdated", "groupsList", "groupDomainVerified", "invitation", "leftMemberUser", "memberAccepted", "membersBlockedForAllUser", "membersRoleUser", "newChatItems", "rcvFileAccepted", "rcvFileAcceptedSndCancelled", "rcvFileCancelled", "remoteCtrlConnected", "remoteCtrlConnecting", "sentConfirmation", "sentGroupInvitation", "sentInvitation", "sentInvitationToContact", "serviceReplyAccepted", "sndFileCancelled", "startedConnectionToContact", "startedConnectionToGroup", "userAcceptedGroupSent", "userContactLink", "userContactLinkCreated", "userContactLinkDeleted", "userContactLinkUpdated", "userDeletedMembers", "userProfileUpdated", "userProfileNoChange", "usersList", "apiChats"]
ChatResponse_Tag = Literal["acceptingContactRequest", "activeUser", "chatItemNotChanged", "chatItemReaction", "chatItemUpdated", "chatItemsDeleted", "chatMsgContent", "chatRunning", "chatStarted", "chatStopped", "cmdOk", "chatCmdError", "connectionPlan", "contactAlreadyExists", "contactConnectionDeleted", "contactDeleted", "contactPrefsUpdated", "contactRequestRejected", "contactsList", "groupDeletedUser", "groupLink", "groupLinkCreated", "groupLinkDeleted", "groupCreated", "publicGroupCreated", "publicGroupCreationFailed", "groupRelays", "groupRelaysAdded", "groupRelaysAddFailed", "relayGroupAllowed", "groupMembers", "groupUpdated", "groupsList", "groupDomainVerified", "invitation", "leftMemberUser", "memberAccepted", "membersBlockedForAllUser", "membersRoleUser", "newChatItems", "rcvFileAccepted", "rcvFileAcceptedSndCancelled", "rcvFileCancelled", "remoteCtrlConnected", "remoteCtrlConnecting", "sentConfirmation", "sentGroupInvitation", "sentGroupInvitationViaGroup", "sentInvitation", "sentInvitationToContact", "serviceReplyAccepted", "sndFileCancelled", "startedConnectionToContact", "startedConnectionToGroup", "userAcceptedGroupSent", "userContactLink", "userContactLinkCreated", "userContactLinkDeleted", "userContactLinkUpdated", "userDeletedMembers", "userProfileUpdated", "userProfileNoChange", "usersList", "apiChats"]
@@ -188,6 +188,9 @@ class BadgeProof(TypedDict):
class BadgeRedeemError_invalidCode(TypedDict):
type: Literal["invalidCode"]
class BadgeRedeemError_invalidReceipt(TypedDict):
type: Literal["invalidReceipt"]
class BadgeRedeemError_serviceNotConfigured(TypedDict):
type: Literal["serviceNotConfigured"]
@@ -210,6 +213,7 @@ class BadgeRedeemError_credentialNotVerified(TypedDict):
BadgeRedeemError = (
BadgeRedeemError_invalidCode
| BadgeRedeemError_invalidReceipt
| BadgeRedeemError_serviceNotConfigured
| BadgeRedeemError_badgeActive
| BadgeRedeemError_serviceError
@@ -218,14 +222,85 @@ BadgeRedeemError = (
| BadgeRedeemError_credentialNotVerified
)
BadgeRedeemError_Tag = Literal["invalidCode", "serviceNotConfigured", "badgeActive", "serviceError", "invalidResponse", "unknownKeyIndex", "credentialNotVerified"]
BadgeRedeemError_Tag = Literal["invalidCode", "invalidReceipt", "serviceNotConfigured", "badgeActive", "serviceError", "invalidResponse", "unknownKeyIndex", "credentialNotVerified"]
BadgeServiceErrorCode = Literal["bad_request", "unsupported_version", "unknown_purchase_key", "unknown_offer_id", "offer_disabled", "offer_mismatch", "product_unavailable", "payment_not_entitled", "payment_pending", "provider_unavailable", "rate_limited", "code_invalid", "code_used", "code_expired", "receipt_invalid", "receipt_used", "internal"]
BadgeServiceErrorCode = Literal["bad_request", "unsupported_version", "unknown_purchase_key", "unknown_offer_id", "offer_disabled", "offer_mismatch", "product_unavailable", "payment_not_entitled", "payment_pending", "provider_unavailable", "provider_not_configured", "rate_limited", "code_invalid", "code_used", "code_expired", "receipt_invalid", "receipt_used", "test_purchase", "internal"]
BadgeStatus = Literal["active", "expired", "expiredOld", "failed", "unknownKey"]
BadgeType = Literal["supporter", "legend", "investor"]
class BlockToken_lineBreak(TypedDict):
type: Literal["lineBreak"]
class BlockToken_softBreak(TypedDict):
type: Literal["softBreak"]
class BlockToken_hardBreak(TypedDict):
type: Literal["hardBreak"]
class BlockToken_paragraphBreak(TypedDict):
type: Literal["paragraphBreak"]
lineBreaks: int # int
class BlockToken_indent(TypedDict):
type: Literal["indent"]
class BlockToken_headingMark(TypedDict):
type: Literal["headingMark"]
level: int # int
class BlockToken_headingClose(TypedDict):
type: Literal["headingClose"]
class BlockToken_headingUnderline(TypedDict):
type: Literal["headingUnderline"]
level: int # int
class BlockToken_listMark(TypedDict):
type: Literal["listMark"]
number: NotRequired[str]
class BlockToken_quoteMark(TypedDict):
type: Literal["quoteMark"]
class BlockToken_fenceMark(TypedDict):
type: Literal["fenceMark"]
language: NotRequired[str]
class BlockToken_rowMark(TypedDict):
type: Literal["rowMark"]
class BlockToken_cellMark(TypedDict):
type: Literal["cellMark"]
class BlockToken_delimiterRow(TypedDict):
type: Literal["delimiterRow"]
columns: list["TableAlign"]
class BlockToken_ruleMark(TypedDict):
type: Literal["ruleMark"]
BlockToken = (
BlockToken_lineBreak
| BlockToken_softBreak
| BlockToken_hardBreak
| BlockToken_paragraphBreak
| BlockToken_indent
| BlockToken_headingMark
| BlockToken_headingClose
| BlockToken_headingUnderline
| BlockToken_listMark
| BlockToken_quoteMark
| BlockToken_fenceMark
| BlockToken_rowMark
| BlockToken_cellMark
| BlockToken_delimiterRow
| BlockToken_ruleMark
)
BlockToken_Tag = Literal["lineBreak", "softBreak", "hardBreak", "paragraphBreak", "indent", "headingMark", "headingClose", "headingUnderline", "listMark", "quoteMark", "fenceMark", "rowMark", "cellMark", "delimiterRow", "ruleMark"]
class BlockingInfo(TypedDict):
reason: "BlockingReason"
notice: NotRequired["ClientNotice"]
@@ -688,6 +763,7 @@ class CIQuote(TypedDict):
sentAt: str # ISO-8601 timestamp
content: "MsgContent"
formattedText: NotRequired[list["FormattedText"]]
markdownBlocks: NotRequired[list["MarkdownBlock"]] # non-empty
class CIReaction(TypedDict):
chatDir: "CIDirection"
@@ -1226,6 +1302,7 @@ class ChatItem(TypedDict):
content: "CIContent"
mentions: dict[str, "CIMention"]
formattedText: NotRequired[list["FormattedText"]]
markdownBlocks: NotRequired[list["MarkdownBlock"]] # non-empty
quotedItem: NotRequired["CIQuote"]
reactions: list["CIReactionCount"]
file: NotRequired["CIFile"]
@@ -1249,7 +1326,7 @@ ChatListQuery = ChatListQuery_filters | ChatListQuery_search
ChatListQuery_Tag = Literal["filters", "search"]
ChatPeerType = Literal["human", "bot", "business"]
ChatPeerType = Literal["human", "bot", "business", "agent"]
# Used in API commands. Chat scope can only be passed with groups.
@@ -1803,8 +1880,11 @@ class FileTransferMeta(TypedDict):
FileType = Literal["normal", "roster"]
# The format markup is a token of block Markdown syntax; its text is the syntax. The format escape is the backslash of a backslash escape. Bold singleStar: written with single stars.
class Format_bold(TypedDict):
type: Literal["bold"]
singleStar: bool
class Format_italic(TypedDict):
type: Literal["italic"]
@@ -1858,6 +1938,13 @@ class Format_email(TypedDict):
class Format_phone(TypedDict):
type: Literal["phone"]
class Format_escape(TypedDict):
type: Literal["escape"]
class Format_markup(TypedDict):
type: Literal["markup"]
token: "BlockToken"
Format = (
Format_bold
| Format_italic
@@ -1874,9 +1961,11 @@ Format = (
| Format_mention
| Format_email
| Format_phone
| Format_escape
| Format_markup
)
Format_Tag = Literal["bold", "italic", "strikeThrough", "snippet", "secret", "small", "colored", "uri", "hyperLink", "simplexLink", "simplexName", "command", "mention", "email", "phone"]
Format_Tag = Literal["bold", "italic", "strikeThrough", "snippet", "secret", "small", "colored", "uri", "hyperLink", "simplexLink", "simplexName", "command", "mention", "email", "phone", "escape", "markup"]
class FormattedText(TypedDict):
format: NotRequired["Format"]
@@ -2209,6 +2298,11 @@ class LinkPreview(TypedDict):
image: str
content: NotRequired["LinkContent"]
class ListEntry(TypedDict):
number: NotRequired[str]
content: "SegmentRange"
items: list["ListEntry"]
class LocalBadge(TypedDict):
badge: "BadgeInfo"
status: "BadgeStatus"
@@ -2228,6 +2322,50 @@ class LocalProfile(TypedDict):
contactDomain: NotRequired["SimplexDomainClaim"]
contactDomainVerified: NotRequired[bool]
class MarkdownBlock_paragraph(TypedDict):
type: Literal["paragraph"]
content: "SegmentRange"
class MarkdownBlock_heading(TypedDict):
type: Literal["heading"]
level: int # int
content: "SegmentRange"
class MarkdownBlock_codeBlock(TypedDict):
type: Literal["codeBlock"]
language: NotRequired[str]
content: "SegmentRange"
class MarkdownBlock_quote(TypedDict):
type: Literal["quote"]
content: "SegmentRange"
class MarkdownBlock_list(TypedDict):
type: Literal["list"]
items: list["ListEntry"] # non-empty
class MarkdownBlock_table(TypedDict):
type: Literal["table"]
columns: list["TableAlign"] # non-empty
header: list["SegmentRange"]
rows: list[list["SegmentRange"]]
class MarkdownBlock_rule(TypedDict):
type: Literal["rule"]
mark: "SegmentRange"
MarkdownBlock = (
MarkdownBlock_paragraph
| MarkdownBlock_heading
| MarkdownBlock_codeBlock
| MarkdownBlock_quote
| MarkdownBlock_list
| MarkdownBlock_table
| MarkdownBlock_rule
)
MarkdownBlock_Tag = Literal["paragraph", "heading", "codeBlock", "quote", "list", "table", "rule"]
MemberCriteria = Literal["all"]
# Connection link sent in a message - only short links are allowed.
@@ -2955,6 +3093,11 @@ class SecurityCode(TypedDict):
securityCode: str
verifiedAt: str # ISO-8601 timestamp
SegmentRange = TypedDict("SegmentRange", {
"from": int, # int
"to": int, # int
})
class SimplePreference(TypedDict):
allow: "FeatureAllowed"
@@ -3591,6 +3734,8 @@ class SupportGroupPreference(TypedDict):
SwitchPhase = Literal["started", "confirmed", "secured", "completed"]
TableAlign = Literal["none", "left", "center", "right"]
class TimedMessagesGroupPreference(TypedDict):
enable: "GroupFeatureEnabled"
ttl: NotRequired[int] # int