simplex-chat 6.5.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- simplex_chat/__init__.py +59 -0
- simplex_chat/__main__.py +35 -0
- simplex_chat/_native.py +257 -0
- simplex_chat/_version.py +9 -0
- simplex_chat/api.py +704 -0
- simplex_chat/bot.py +707 -0
- simplex_chat/core.py +200 -0
- simplex_chat/filters.py +45 -0
- simplex_chat/py.typed +0 -0
- simplex_chat/types/__init__.py +16 -0
- simplex_chat/types/_commands.py +705 -0
- simplex_chat/types/_events.py +379 -0
- simplex_chat/types/_responses.py +360 -0
- simplex_chat/types/_types.py +3506 -0
- simplex_chat/util.py +128 -0
- simplex_chat-6.5.1.dist-info/METADATA +98 -0
- simplex_chat-6.5.1.dist-info/RECORD +19 -0
- simplex_chat-6.5.1.dist-info/WHEEL +4 -0
- simplex_chat-6.5.1.dist-info/licenses/LICENSE +661 -0
simplex_chat/util.py
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"""Reusable helpers for working with chat events, types, and message content.
|
|
2
|
+
|
|
3
|
+
Mirrors the Node `util.ts` exports — provides the same primitives bot
|
|
4
|
+
authors typically reach for: command parsing, sender display strings,
|
|
5
|
+
message-content extraction, profile field cleanup, and ChatRef extraction
|
|
6
|
+
from a ChatInfo (handy when echoing into a different chat).
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import re
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
from .types import T
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def chat_info_ref(c_info: T.ChatInfo) -> T.ChatRef | None:
|
|
18
|
+
"""Extract a wire-format `ChatRef` from a `ChatInfo`.
|
|
19
|
+
|
|
20
|
+
Returns `None` for non-chat infos (contactRequest, contactConnection)
|
|
21
|
+
that can't be the target of `api_send_messages`. For groups, the
|
|
22
|
+
`memberSupport` scope is forwarded so messages land in the right
|
|
23
|
+
thread; other scopes are dropped (matches Node `util.chatInfoRef`).
|
|
24
|
+
"""
|
|
25
|
+
t = c_info["type"]
|
|
26
|
+
if t == "direct":
|
|
27
|
+
return {"chatType": "direct", "chatId": c_info["contact"]["contactId"]} # type: ignore[index]
|
|
28
|
+
if t == "group":
|
|
29
|
+
ref: T.ChatRef = {"chatType": "group", "chatId": c_info["groupInfo"]["groupId"]} # type: ignore[index]
|
|
30
|
+
scope = c_info.get("groupChatScope") # type: ignore[union-attr]
|
|
31
|
+
if scope and scope.get("type") == "memberSupport":
|
|
32
|
+
member = scope.get("groupMember_")
|
|
33
|
+
ms_scope: T.GroupChatScope_memberSupport = {"type": "memberSupport"}
|
|
34
|
+
if member is not None:
|
|
35
|
+
ms_scope["groupMemberId_"] = member["groupMemberId"]
|
|
36
|
+
ref["chatScope"] = ms_scope
|
|
37
|
+
return ref
|
|
38
|
+
return None
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def chat_info_name(c_info: T.ChatInfo) -> str:
|
|
42
|
+
"""Display string for a chat: `@Alice`, `#GroupName`, `private notes`, etc."""
|
|
43
|
+
t = c_info["type"]
|
|
44
|
+
if t == "direct":
|
|
45
|
+
return f"@{c_info['contact']['profile']['displayName']}" # type: ignore[index]
|
|
46
|
+
if t == "group":
|
|
47
|
+
scope = c_info.get("groupChatScope") # type: ignore[union-attr]
|
|
48
|
+
if scope and scope.get("type") == "memberSupport":
|
|
49
|
+
member = scope.get("groupMember_")
|
|
50
|
+
scope_name = f" {member['memberProfile']['displayName']}" if member else ""
|
|
51
|
+
return f"#{c_info['groupInfo']['groupProfile']['displayName']}(support{scope_name})" # type: ignore[index]
|
|
52
|
+
return f"#{c_info['groupInfo']['groupProfile']['displayName']}" # type: ignore[index]
|
|
53
|
+
if t == "local":
|
|
54
|
+
return "private notes"
|
|
55
|
+
if t == "contactRequest":
|
|
56
|
+
return f"request from @{c_info['contactRequest']['profile']['displayName']}" # type: ignore[index]
|
|
57
|
+
if t == "contactConnection":
|
|
58
|
+
alias = c_info["contactConnection"].get("localAlias") # type: ignore[index]
|
|
59
|
+
return f"pending connection ({alias})" if alias else "pending connection"
|
|
60
|
+
return f"<{t}>"
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def sender_name(c_info: T.ChatInfo, chat_dir: T.CIDirection) -> str:
|
|
64
|
+
"""Sender display: chat name plus group sender suffix when applicable."""
|
|
65
|
+
base = chat_info_name(c_info)
|
|
66
|
+
if chat_dir["type"] == "groupRcv":
|
|
67
|
+
sender = chat_dir["groupMember"]["memberProfile"]["displayName"] # type: ignore[index]
|
|
68
|
+
return f"{base} @{sender}"
|
|
69
|
+
return base
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def contact_address_str(link: T.CreatedConnLink) -> str:
|
|
73
|
+
"""Prefer the short link, fall back to the full link."""
|
|
74
|
+
return link.get("connShortLink") or link["connFullLink"]
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def from_local_profile(local: T.LocalProfile) -> T.Profile:
|
|
78
|
+
"""Strip extra LocalProfile fields (profileId, localAlias) and undefined values."""
|
|
79
|
+
p: dict[str, Any] = {}
|
|
80
|
+
for key in (
|
|
81
|
+
"displayName",
|
|
82
|
+
"fullName",
|
|
83
|
+
"shortDescr",
|
|
84
|
+
"image",
|
|
85
|
+
"contactLink",
|
|
86
|
+
"preferences",
|
|
87
|
+
"peerType",
|
|
88
|
+
):
|
|
89
|
+
v = local.get(key) # type: ignore[misc]
|
|
90
|
+
if v is not None:
|
|
91
|
+
p[key] = v
|
|
92
|
+
return p # type: ignore[return-value]
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def ci_content_text(chat_item: T.ChatItem) -> str | None:
|
|
96
|
+
"""Extract the message text from a sent or received message item, if any."""
|
|
97
|
+
content = chat_item["content"]
|
|
98
|
+
if content["type"] in ("sndMsgContent", "rcvMsgContent"):
|
|
99
|
+
msg = content.get("msgContent", {}) # type: ignore[union-attr]
|
|
100
|
+
return msg.get("text")
|
|
101
|
+
return None
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
_BOT_COMMAND_RE = re.compile(r"^/([^\s]+)(.*)$")
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def ci_bot_command(chat_item: T.ChatItem) -> tuple[str, str] | None:
|
|
108
|
+
"""Parse a `/keyword args...` slash-command from a chat item.
|
|
109
|
+
|
|
110
|
+
Returns `(keyword, trimmed_params)` or `None` if the message isn't a
|
|
111
|
+
slash command. Mirrors Node `util.ciBotCommand` semantics.
|
|
112
|
+
"""
|
|
113
|
+
text = ci_content_text(chat_item)
|
|
114
|
+
if not text:
|
|
115
|
+
return None
|
|
116
|
+
text = text.strip()
|
|
117
|
+
m = _BOT_COMMAND_RE.match(text)
|
|
118
|
+
if not m:
|
|
119
|
+
return None
|
|
120
|
+
return m.group(1), m.group(2).strip()
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def reaction_text(reaction: T.ACIReaction) -> str:
|
|
124
|
+
"""Format an `ACIReaction` as the emoji character or tag string."""
|
|
125
|
+
r = reaction["chatReaction"]["reaction"] # type: ignore[index]
|
|
126
|
+
if r["type"] == "emoji":
|
|
127
|
+
return r["emoji"] # type: ignore[index]
|
|
128
|
+
return r.get("tag", "") # type: ignore[union-attr]
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: simplex-chat
|
|
3
|
+
Version: 6.5.1
|
|
4
|
+
Summary: SimpleX Chat Python library for chat bots
|
|
5
|
+
Project-URL: Homepage, https://github.com/simplex-chat/simplex-chat/tree/stable/packages/simplex-chat-python
|
|
6
|
+
Project-URL: Issues, https://github.com/simplex-chat/simplex-chat/issues
|
|
7
|
+
Author: SimpleX Chat
|
|
8
|
+
License-Expression: AGPL-3.0-only
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Keywords: bots,chat,messenger,privacy,security,simplex
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: License :: OSI Approved :: GNU Affero General Public License v3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Classifier: Topic :: Communications :: Chat
|
|
18
|
+
Requires-Python: >=3.11
|
|
19
|
+
Provides-Extra: dev
|
|
20
|
+
Requires-Dist: pyright>=1.1.380; extra == 'dev'
|
|
21
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
|
|
22
|
+
Requires-Dist: pytest>=8; extra == 'dev'
|
|
23
|
+
Requires-Dist: ruff>=0.6; extra == 'dev'
|
|
24
|
+
Provides-Extra: test
|
|
25
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == 'test'
|
|
26
|
+
Requires-Dist: pytest>=8; extra == 'test'
|
|
27
|
+
Description-Content-Type: text/markdown
|
|
28
|
+
|
|
29
|
+
# SimpleX Chat Python library
|
|
30
|
+
|
|
31
|
+
Python 3.11+ client for [SimpleX Chat](https://simplex.chat) bots. Equivalent to the [Node.js library](https://www.npmjs.com/package/simplex-chat).
|
|
32
|
+
|
|
33
|
+
## Install
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
pip install simplex-chat
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
The native `libsimplex` is downloaded lazily on first use. To pre-fetch:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
python -m simplex_chat install # sqlite (default)
|
|
43
|
+
python -m simplex_chat install --backend postgres # linux-x86_64 only
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Quick start
|
|
47
|
+
|
|
48
|
+
```python
|
|
49
|
+
import re
|
|
50
|
+
from simplex_chat import Bot, BotProfile, Message, SqliteDb, TextMessage
|
|
51
|
+
|
|
52
|
+
bot = Bot(
|
|
53
|
+
profile=BotProfile(display_name="Squaring bot"),
|
|
54
|
+
db=SqliteDb(file_prefix="./squaring_bot"),
|
|
55
|
+
welcome="Send me a number, I'll square it.",
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
@bot.on_message(content_type="text", text=re.compile(r"^-?\d+(\.\d+)?$"))
|
|
59
|
+
async def square(msg: TextMessage) -> None:
|
|
60
|
+
n = float(msg.text or "0")
|
|
61
|
+
await msg.reply(f"{n} * {n} = {n * n}")
|
|
62
|
+
|
|
63
|
+
@bot.on_message(content_type="text")
|
|
64
|
+
async def fallback(msg: Message) -> None:
|
|
65
|
+
await msg.reply("Send me a number, like 7 or 3.14.")
|
|
66
|
+
|
|
67
|
+
if __name__ == "__main__":
|
|
68
|
+
bot.run()
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
`bot.run()` blocks. The connection address is logged on startup — paste it into a SimpleX client to talk to the bot. `Ctrl+C` to stop.
|
|
72
|
+
|
|
73
|
+
Three decorators: `@bot.on_message(...)`, `@bot.on_command(name)`, `@bot.on_event(tag)`. Message handlers are first-match-wins in registration order, so register specific filters first and catch-alls last.
|
|
74
|
+
|
|
75
|
+
See [`examples/squaring_bot.py`](./examples/squaring_bot.py) for the full example.
|
|
76
|
+
|
|
77
|
+
## Development
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
uv venv && source .venv/bin/activate
|
|
81
|
+
uv pip install -e '.[dev]'
|
|
82
|
+
ruff check && pyright && pytest tests/
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Wire types under `src/simplex_chat/types/_*.py` are generated. Regenerate with `cabal test simplex-chat-test --test-options='--match Python'`.
|
|
86
|
+
|
|
87
|
+
## Release
|
|
88
|
+
|
|
89
|
+
Manual for now. Bump `_version.py:__version__`, build a wheel, upload to PyPI:
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
uv build --wheel
|
|
93
|
+
uv publish --token "$PYPI_TOKEN"
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## License
|
|
97
|
+
|
|
98
|
+
[AGPL-3.0](./LICENSE)
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
simplex_chat/__init__.py,sha256=FezBxC3pazBkSFdUT1b3UkWY1LqI7kboJYT3402djI8,1226
|
|
2
|
+
simplex_chat/__main__.py,sha256=LtJshOx8sXoRbHJdPd2zmDruBuZkQ7l4JqQ2IENVxZs,1097
|
|
3
|
+
simplex_chat/_native.py,sha256=cjMxOTpOGkiF_tWSroSHsUUcq39XnAJlw6AslLWOlJg,9721
|
|
4
|
+
simplex_chat/_version.py,sha256=G_nG42RX9kJkzCxAmKIH4lRZ39FaiT0VdZ1KLx_nCj8,410
|
|
5
|
+
simplex_chat/api.py,sha256=sklZfJCFlKXSYJetu6CE_szSxU8M_fwY9ZvPxxnM4gE,28234
|
|
6
|
+
simplex_chat/bot.py,sha256=yWyTCQAmO7DxBZIEDzPC4m0FonQqPhD2YUdWTxAQEzE,27306
|
|
7
|
+
simplex_chat/core.py,sha256=eLwiPIlrA1bHKEHRxQie26k_LSRKtnJTn6s-hpAK-h0,7091
|
|
8
|
+
simplex_chat/filters.py,sha256=iYSE9a0THMf53dGoi0QJqQSHBgXkImeMWGg98Nuq9d8,1610
|
|
9
|
+
simplex_chat/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
10
|
+
simplex_chat/util.py,sha256=y3pSzrr4dhDR0s5xX6UdD5b3K0HQp4cQ06HnOcCVOI8,5029
|
|
11
|
+
simplex_chat/types/__init__.py,sha256=zSqRDg53agVD6TY2fkzM3XeK7YY9yujv3e03_cay2Jo,580
|
|
12
|
+
simplex_chat/types/_commands.py,sha256=qKSGkqqfatjGPp0-21EoEVO6XM6CObl6OreQH0XS2ag,22068
|
|
13
|
+
simplex_chat/types/_events.py,sha256=HT4uxItdYLjcASEuKPQaZUpwmC3O2lziW3ZDBUCV_Cg,10959
|
|
14
|
+
simplex_chat/types/_responses.py,sha256=iUpZ8HHW_BOXJWVXMdYWs83yRsHS2AgY0xvHge2YINU,10186
|
|
15
|
+
simplex_chat/types/_types.py,sha256=_f3beWqpIwUkCwRFByNfYhKOB3AKpr1fuGDHOVXU6s4,103439
|
|
16
|
+
simplex_chat-6.5.1.dist-info/METADATA,sha256=6tUSEbC7rbp114U6vUU0xOsSxmao_vN7hdUr5HNcXqI,3170
|
|
17
|
+
simplex_chat-6.5.1.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
18
|
+
simplex_chat-6.5.1.dist-info/licenses/LICENSE,sha256=hIahDEOTzuHCU5J2nd07LWwkLW7Hko4UFO__ffsvB-8,34523
|
|
19
|
+
simplex_chat-6.5.1.dist-info/RECORD,,
|