Merge branch 'master' into f/public-groups

This commit is contained in:
spaced4ndy
2026-06-02 12:49:41 +04:00
115 changed files with 4737 additions and 1064 deletions
@@ -33,8 +33,38 @@ from .types import T
@dataclass(slots=True)
class BotCommand:
"""One entry in the bot's advertised slash-command list (wire-side
`groupPreferences.commands` or profile `preferences.commands`).
`keyword` and `label` are required: `keyword` is what the user
types after `/`; `label` is the human-readable description shown
next to the keyword in the SimpleX client's commands menu.
`params` is an optional placeholder string that controls how the
client behaves when the user taps the command in the menu:
* `params=None` (default) — the client SENDS `/<keyword>`
immediately on tap; no input-box detour. Use this for
zero-argument commands (`/help`, `/ping`) where the action is
unambiguous.
* `params="<value>"` — the client PASTES `/<keyword> <params>`
into the input box and positions the cursor at the end. The
user edits the placeholder and sends. Use this for commands
that take a required argument (`/review <pr-url>`,
`/order <number>`) so the user sees the expected shape
without having to remember it.
Mirrors `CBCCommand` in the Haskell core
(`Simplex.Chat.Types.Preferences`) and the wire TypedDict
`ChatBotCommand_command`. Both SimpleX clients (Android/Kotlin
and iOS/Swift) implement the paste-vs-send branch on the
`params` field; see `CommandsMenuView.{kt,swift}` for the
reference UI behaviour.
"""
keyword: str
label: str
params: str | None = None
class Bot(Client):
@@ -145,10 +175,24 @@ class Bot(Client):
"files": {"allow": "yes" if self._allow_files else "no"},
}
if self._commands:
prefs["commands"] = [
{"type": "command", "keyword": c.keyword, "label": c.label}
for c in self._commands
]
cmds: list[T.ChatBotCommand] = []
for c in self._commands:
entry: T.ChatBotCommand_command = {
"type": "command",
"keyword": c.keyword,
"label": c.label,
}
# `params` is `NotRequired[str]` on the wire; omit the
# key entirely when None so the Haskell parser sees
# `Nothing` rather than `Just ""`. The two have
# different client semantics: `Nothing` (`params=None`)
# triggers an immediate send on tap; `Just ""` would
# paste `/<keyword> ` (with a trailing space) into the
# input box, which is rarely what the operator wants.
if c.params is not None:
entry["params"] = c.params
cmds.append(entry)
prefs["commands"] = cmds
p["preferences"] = prefs
p["peerType"] = "bot"
return p
@@ -1683,6 +1683,10 @@ class Format_simplexLink(TypedDict):
simplexUri: str
smpHosts: list[str] # non-empty
class Format_simplexName(TypedDict):
type: Literal["simplexName"]
nameInfo: "SimplexNameInfo"
class Format_command(TypedDict):
type: Literal["command"]
commandStr: str
@@ -1708,13 +1712,14 @@ Format = (
| Format_uri
| Format_hyperLink
| Format_simplexLink
| Format_simplexName
| Format_command
| Format_mention
| Format_email
| Format_phone
)
Format_Tag = Literal["bold", "italic", "strikeThrough", "snippet", "secret", "small", "colored", "uri", "hyperLink", "simplexLink", "command", "mention", "email", "phone"]
Format_Tag = Literal["bold", "italic", "strikeThrough", "snippet", "secret", "small", "colored", "uri", "hyperLink", "simplexLink", "simplexName", "command", "mention", "email", "phone"]
class FormattedText(TypedDict):
format: NotRequired["Format"]
@@ -1851,6 +1856,10 @@ class GroupLinkPlan_noRelays(TypedDict):
type: Literal["noRelays"]
groupSLinkData_: NotRequired["GroupShortLinkData"]
class GroupLinkPlan_updateRequired(TypedDict):
type: Literal["updateRequired"]
groupSLinkData_: NotRequired["GroupShortLinkData"]
GroupLinkPlan = (
GroupLinkPlan_ok
| GroupLinkPlan_ownLink
@@ -1858,9 +1867,10 @@ GroupLinkPlan = (
| GroupLinkPlan_connectingProhibit
| GroupLinkPlan_known
| GroupLinkPlan_noRelays
| GroupLinkPlan_updateRequired
)
GroupLinkPlan_Tag = Literal["ok", "ownLink", "connectingConfirmReconnect", "connectingProhibit", "known", "noRelays"]
GroupLinkPlan_Tag = Literal["ok", "ownLink", "connectingConfirmReconnect", "connectingProhibit", "known", "noRelays", "updateRequired"]
class GroupMember(TypedDict):
groupMemberId: int # int64
@@ -1936,6 +1946,7 @@ class GroupRelay(TypedDict):
userChatRelay: "UserChatRelay"
relayStatus: "RelayStatus"
relayLink: NotRequired[str]
relayCap: "RelayCapabilities"
class GroupRootKey_private(TypedDict):
type: Literal["private"]
@@ -2344,6 +2355,12 @@ ProxyError = ProxyError_PROTOCOL | ProxyError_BROKER | ProxyError_BASIC_AUTH | P
ProxyError_Tag = Literal["PROTOCOL", "BROKER", "BASIC_AUTH", "NO_SESSION"]
class PublicGroupAccess(TypedDict):
groupWebPage: NotRequired[str]
groupDomain: NotRequired[str]
domainWebPage: bool
allowEmbedding: bool
class PublicGroupData(TypedDict):
publicMemberCount: int # int64
@@ -2351,6 +2368,7 @@ class PublicGroupProfile(TypedDict):
groupType: "GroupType"
groupLink: str
publicGroupId: str
publicGroupAccess: NotRequired["PublicGroupAccess"]
class RCErrorType_internal(TypedDict):
type: Literal["internal"]
@@ -2619,6 +2637,9 @@ RcvMsgError = RcvMsgError_dropped | RcvMsgError_parseError
RcvMsgError_Tag = Literal["dropped", "parseError"]
class RelayCapabilities(TypedDict):
webDomain: NotRequired[str]
class RelayProfile(TypedDict):
displayName: str
fullName: str
@@ -2680,6 +2701,19 @@ class SimplePreference(TypedDict):
SimplexLinkType = Literal["contact", "invitation", "group", "channel", "relay"]
class SimplexNameDomain(TypedDict):
nameTLD: "SimplexTLD"
domain: str
subDomain: list[str]
class SimplexNameInfo(TypedDict):
nameType: "SimplexNameType"
nameDomain: "SimplexNameDomain"
SimplexNameType = Literal["publicGroup", "contact"]
SimplexTLD = Literal["simplex", "testing", "web"]
SndCIStatusProgress = Literal["partial", "complete"]
class SndConnEvent_switchQueue(TypedDict):