mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-05-28 11:35:24 +00:00
9584992c83
* simplex-chat-python: split Client from Bot, add request/response API
Client is now the base class for SimpleX participants that talk TO
services (monitors, probes, automated participants). Bot extends Client
with server features (address, auto-accept, welcome, commands).
New methods on Client (inherited by Bot):
connect_to(link) idempotent contact handshake
send_and_wait(id, text) send a message and await the reply
events() async iterator over chat events
@on_message(contact_id=N) filter by sender in decorators
BotProfile renamed to Profile (alias kept). New ContactAlreadyExistsError
subclass for cleaner error handling.
* simplex-chat-python: narrow event payload type per @on_event tag
@client.on_event("contactConnected") now types the handler's event
parameter as CEvt.ContactConnected instead of the unnarrowed
CEvt.ChatEvent union — mirroring how @on_message narrows by
content_type.
The 50 overloads are generated by the Haskell codegen into _events.py
(as a Protocol class), so new events stay in sync automatically.
Client.on_event is exposed as a property typed as that Protocol; the
runtime implementation is unchanged.
179 lines
5.6 KiB
Python
179 lines
5.6 KiB
Python
"""`Bot` — Client extended with server-side features (address, auto-accept, commands)."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass
|
|
|
|
from . import util
|
|
from .api import Db
|
|
from .client import (
|
|
BotProfile,
|
|
ChatMessage,
|
|
Client,
|
|
CommandHandler,
|
|
EventHandler,
|
|
FileMessage,
|
|
ImageMessage,
|
|
LinkMessage,
|
|
Message,
|
|
MessageHandler,
|
|
Middleware,
|
|
ParsedCommand,
|
|
Profile,
|
|
ReportMessage,
|
|
TextMessage,
|
|
UnknownMessage,
|
|
VideoMessage,
|
|
VoiceMessage,
|
|
log,
|
|
)
|
|
from .core import MigrationConfirmation
|
|
from .types import T
|
|
|
|
|
|
@dataclass(slots=True)
|
|
class BotCommand:
|
|
keyword: str
|
|
label: str
|
|
|
|
|
|
class Bot(Client):
|
|
"""SimpleX bot — Client extended with server-side features.
|
|
|
|
On top of `Client` (identity + messaging + connect_to/send_and_wait/events),
|
|
a Bot:
|
|
- creates and announces its own contact address
|
|
- auto-accepts incoming contact requests (configurable)
|
|
- advertises a list of slash-commands in its profile preferences
|
|
- sets `peerType=bot` and disables calls/voice in profile prefs
|
|
- sends a `welcome` message to new contacts via the auto-reply address setting
|
|
|
|
If you want just identity + messaging without any of that, use `Client`
|
|
directly.
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
*,
|
|
profile: Profile,
|
|
db: Db,
|
|
welcome: str | T.MsgContent | None = None,
|
|
commands: list[BotCommand] | None = None,
|
|
confirm_migrations: MigrationConfirmation = MigrationConfirmation.YES_UP,
|
|
create_address: bool = True,
|
|
update_address: bool = True,
|
|
update_profile: bool = True,
|
|
auto_accept: bool = True,
|
|
business_address: bool = False,
|
|
allow_files: bool = False,
|
|
log_contacts: bool = True,
|
|
log_network: bool = False,
|
|
) -> None:
|
|
super().__init__(
|
|
profile=profile,
|
|
db=db,
|
|
confirm_migrations=confirm_migrations,
|
|
update_profile=update_profile,
|
|
log_contacts=log_contacts,
|
|
log_network=log_network,
|
|
)
|
|
self._welcome = welcome
|
|
self._commands = commands or []
|
|
self._create_address = create_address
|
|
self._update_address = update_address
|
|
self._auto_accept = auto_accept
|
|
self._business_address = business_address
|
|
self._allow_files = allow_files
|
|
|
|
# ------------------------------------------------------------------ #
|
|
# Profile + address sync (overrides hooks in Client)
|
|
# ------------------------------------------------------------------ #
|
|
|
|
async def _post_start(self, user: T.User) -> None:
|
|
"""Bots sync address first, then embed the link in the profile."""
|
|
link = await self._sync_address(user)
|
|
await self._maybe_sync_profile(user, contact_link=link)
|
|
|
|
async def _sync_address(self, user: T.User) -> str | None:
|
|
"""Address sync. Returns the public link if any, for embedding in the profile."""
|
|
api = self.api
|
|
user_id = user["userId"]
|
|
|
|
address = await api.api_get_user_address(user_id)
|
|
if address is None:
|
|
if self._create_address:
|
|
log.info("Bot has no address, creating...")
|
|
await api.api_create_user_address(user_id)
|
|
address = await api.api_get_user_address(user_id)
|
|
if address is None:
|
|
raise RuntimeError("Failed reading newly created user address")
|
|
else:
|
|
log.warning("Bot has no address")
|
|
|
|
link: str | None = None
|
|
if address is not None:
|
|
link = util.contact_address_str(address["connLinkContact"])
|
|
log.info("Bot address: %s", link)
|
|
|
|
# Address settings (auto-accept + welcome message). Mirrors bot.ts:185-194.
|
|
# autoAccept present → accept; absent → no auto-accept (mirrors Node bot.ts).
|
|
if address is not None and self._update_address:
|
|
desired: T.AddressSettings = {"businessAddress": self._business_address}
|
|
if self._auto_accept:
|
|
desired["autoAccept"] = {"acceptIncognito": False}
|
|
if self._welcome is not None:
|
|
desired["autoReply"] = (
|
|
{"type": "text", "text": self._welcome}
|
|
if isinstance(self._welcome, str)
|
|
else self._welcome
|
|
)
|
|
if address["addressSettings"] != desired:
|
|
log.info("Bot address settings changed, updating...")
|
|
await api.api_set_address_settings(user_id, desired)
|
|
|
|
return link
|
|
|
|
def _profile_to_wire(self) -> T.Profile:
|
|
"""Bot profile: base profile + peerType=bot, command list, calls/voice prefs disabled.
|
|
|
|
Mirrors Node `mkBotProfile` (bot.ts:88-102).
|
|
"""
|
|
p = super()._profile_to_wire()
|
|
prefs: T.Preferences = {
|
|
"calls": {"allow": "no"},
|
|
"voice": {"allow": "no"},
|
|
"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
|
|
]
|
|
p["preferences"] = prefs
|
|
p["peerType"] = "bot"
|
|
return p
|
|
|
|
|
|
__all__ = [
|
|
"Bot",
|
|
"BotCommand",
|
|
"BotProfile",
|
|
"ChatMessage",
|
|
"Client",
|
|
"CommandHandler",
|
|
"EventHandler",
|
|
"FileMessage",
|
|
"ImageMessage",
|
|
"LinkMessage",
|
|
"Message",
|
|
"MessageHandler",
|
|
"Middleware",
|
|
"ParsedCommand",
|
|
"Profile",
|
|
"ReportMessage",
|
|
"TextMessage",
|
|
"UnknownMessage",
|
|
"VideoMessage",
|
|
"VoiceMessage",
|
|
]
|