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/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,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.29.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any