Skip to content

Commit 8c898a0

Browse files
dmitrymihalevDmitriyMikhalev
authored andcommitted
feat: Add method to create thread to Bot
1 parent 9219e2e commit 8c898a0

File tree

6 files changed

+329
-7
lines changed

6 files changed

+329
-7
lines changed

pybotx/__init__.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,9 @@
3939
ChatCreationError,
4040
ChatCreationProhibitedError,
4141
InvalidUsersListError,
42+
ThreadCreationError,
43+
ThreadCreationEventNotFoundError,
44+
ThreadCreationProhibitedError,
4245
)
4346
from pybotx.client.exceptions.common import (
4447
ChatNotFoundError,
@@ -260,6 +263,9 @@
260263
"SyncSmartAppEventHandlerFunc",
261264
"SyncSmartAppEventHandlerNotFoundError",
262265
"SyncSourceTypes",
266+
"ThreadCreationError",
267+
"ThreadCreationEventNotFoundError",
268+
"ThreadCreationProhibitedError",
263269
"UnknownBotAccountError",
264270
"UnknownSystemEventError",
265271
"UnsupportedBotAPIVersionError",

pybotx/bot/bot.py

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,13 @@
11
from asyncio import Task
2+
from collections.abc import AsyncIterable, AsyncIterator, Iterator, Mapping, Sequence
23
from contextlib import asynccontextmanager
34
from datetime import datetime
45
from types import SimpleNamespace
56
from typing import (
67
Any,
7-
AsyncIterable,
8-
AsyncIterator,
98
Dict,
10-
Iterator,
119
List,
12-
Mapping,
1310
Optional,
14-
Sequence,
1511
Set,
1612
Tuple,
1713
Union,
@@ -57,6 +53,10 @@
5753
BotXAPICreateChatRequestPayload,
5854
CreateChatMethod,
5955
)
56+
from pybotx.client.chats_api.create_thread import (
57+
BotXAPICreateThreadRequestPayload,
58+
CreateThreadMethod,
59+
)
6060
from pybotx.client.chats_api.disable_stealth import (
6161
BotXAPIDisableStealthRequestPayload,
6262
DisableStealthMethod,
@@ -1176,6 +1176,27 @@ async def create_chat(
11761176

11771177
return botx_api_chat_id.to_domain()
11781178

1179+
async def create_thread(self, bot_id: UUID, sync_id: UUID) -> UUID:
1180+
"""
1181+
Create thread.
1182+
1183+
:param bot_id: Bot which should perform the request.
1184+
:param sync_id: Message for which thread should be created
1185+
1186+
:return: Created thread uuid.
1187+
"""
1188+
1189+
method = CreateThreadMethod(
1190+
bot_id,
1191+
self._httpx_client,
1192+
self._bot_accounts_storage,
1193+
)
1194+
1195+
payload = BotXAPICreateThreadRequestPayload.from_domain(sync_id=sync_id)
1196+
botx_api_thread_id = await method.execute(payload)
1197+
1198+
return botx_api_thread_id.to_domain()
1199+
11791200
async def pin_message(
11801201
self,
11811202
*,
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import typing
2+
from typing import Literal
3+
from uuid import UUID
4+
5+
from pybotx.client.authorized_botx_method import AuthorizedBotXMethod
6+
from pybotx.client.botx_method import response_exception_thrower
7+
from pybotx.client.exceptions.chats import (
8+
ThreadCreationError,
9+
ThreadCreationEventNotFoundError,
10+
ThreadCreationProhibitedError,
11+
)
12+
from pybotx.models.api_base import UnverifiedPayloadBaseModel, VerifiedPayloadBaseModel
13+
14+
15+
class BotXAPICreateThreadRequestPayload(UnverifiedPayloadBaseModel):
16+
sync_id: UUID
17+
18+
@classmethod
19+
def from_domain(cls, sync_id: UUID) -> "BotXAPICreateThreadRequestPayload":
20+
return cls(sync_id=sync_id)
21+
22+
23+
class BotXAPIThreadIdResult(VerifiedPayloadBaseModel):
24+
thread_id: UUID
25+
26+
27+
class BotXAPICreateThreadResponsePayload(VerifiedPayloadBaseModel):
28+
status: Literal["ok"]
29+
result: BotXAPIThreadIdResult
30+
31+
def to_domain(self) -> UUID:
32+
return self.result.thread_id
33+
34+
35+
class CreateThreadMethod(AuthorizedBotXMethod):
36+
status_handlers: typing.ClassVar = {
37+
**AuthorizedBotXMethod.status_handlers,
38+
403: response_exception_thrower(ThreadCreationProhibitedError),
39+
404: response_exception_thrower(ThreadCreationEventNotFoundError),
40+
422: response_exception_thrower(ThreadCreationError),
41+
}
42+
43+
async def execute(
44+
self,
45+
payload: BotXAPICreateThreadRequestPayload,
46+
) -> BotXAPICreateThreadResponsePayload:
47+
path = "/api/v3/botx/chats/create_thread"
48+
49+
response = await self._botx_method_call(
50+
"POST",
51+
self._build_url(path),
52+
json=payload.jsonable_dict(),
53+
)
54+
55+
return self._verify_and_extract_api_model(
56+
BotXAPICreateThreadResponsePayload,
57+
response,
58+
)

pybotx/client/exceptions/chats.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,26 @@ class ChatCreationProhibitedError(BaseClientError):
1515

1616
class ChatCreationError(BaseClientError):
1717
"""Error while chat creation."""
18+
19+
20+
class ThreadCreationError(BaseClientError):
21+
"""Error while thread creation (invalid scheme)."""
22+
23+
24+
class ThreadCreationProhibitedError(BaseClientError):
25+
"""
26+
1. Bot has no permissions to create thread
27+
2. Threads are not allowed for that message
28+
3. Bot is not a chat member where message is located
29+
4. Message is located in personal chat
30+
5. Usupported event type
31+
6. Unsuppoerted chat type
32+
7. Thread is already created
33+
8. No access for message
34+
9. Message in stealth mode
35+
10. Message is deleted
36+
"""
37+
38+
39+
class ThreadCreationEventNotFoundError(BaseClientError):
40+
"""Event not found"""

pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
[tool.poetry]
22
name = "pybotx"
3-
version = "0.75.1"
3+
version = "0.75.2"
44
description = "A python library for interacting with eXpress BotX API"
55
authors = [
66
"Sidnev Nikolay <nsidnev@ccsteam.ru>",
77
"Maxim Gorbachev <mgorbachev@ccsteam.ru>",
88
"Alexander Samoylenko <alexandr.samojlenko@ccsteam.ru>",
9-
"Arseniy Zhiltsov <arseniy.zhiltsov@ccsteam.ru>"
9+
"Arseniy Zhiltsov <arseniy.zhiltsov@ccsteam.ru>",
1010
]
1111
readme = "README.md"
1212
repository = "https://github.com/ExpressApp/pybotx"
Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
1+
from http import HTTPStatus
2+
from uuid import UUID
3+
4+
import httpx
5+
import pytest
6+
from respx.router import MockRouter
7+
8+
from pybotx import (
9+
Bot,
10+
BotAccountWithSecret,
11+
HandlerCollector,
12+
ThreadCreationError,
13+
ThreadCreationEventNotFoundError,
14+
ThreadCreationProhibitedError,
15+
lifespan_wrapper,
16+
)
17+
18+
pytestmark = [
19+
pytest.mark.asyncio,
20+
pytest.mark.mock_authorization,
21+
pytest.mark.usefixtures("respx_mock"),
22+
]
23+
24+
ENDPOINT = "api/v3/botx/chats/create_thread"
25+
26+
27+
async def test__create_chat__succeed(
28+
respx_mock: MockRouter,
29+
host: str,
30+
bot_id: UUID,
31+
bot_account: BotAccountWithSecret,
32+
) -> None:
33+
# - Arrange -
34+
sync_id = "21a9ec9e-f21f-4406-ac44-1a78d2ccf9e3"
35+
thread_id = "2a8c0d1e-c4d1-4308-b024-6e1a9f4a4b6d"
36+
endpoint = respx_mock.post(
37+
f"https://{host}/{ENDPOINT}",
38+
headers={"Authorization": "Bearer token", "Content-Type": "application/json"},
39+
json={"sync_id": sync_id},
40+
).mock(
41+
return_value=httpx.Response(
42+
HTTPStatus.OK,
43+
json={
44+
"status": "ok",
45+
"result": {"thread_id": thread_id},
46+
},
47+
),
48+
)
49+
50+
built_bot = Bot(collectors=[HandlerCollector()], bot_accounts=[bot_account])
51+
52+
# - Act -
53+
async with lifespan_wrapper(built_bot) as bot:
54+
created_thread_id = await bot.create_thread(bot_id=bot_id, sync_id=UUID(sync_id))
55+
56+
# - Assert -
57+
assert str(created_thread_id) == thread_id
58+
assert endpoint.called
59+
60+
61+
@pytest.mark.parametrize(
62+
"return_json, response_status, expected_exc_type",
63+
(
64+
(
65+
{
66+
"status": "error",
67+
"reason": "thread_creation_is_prohibited",
68+
"errors": ["This bot is not allowed to create thread"],
69+
"error_data": {"bot_id": "24348246-6791-4ac0-9d86-b948cd6a0e46"},
70+
},
71+
HTTPStatus.FORBIDDEN,
72+
ThreadCreationProhibitedError,
73+
),
74+
(
75+
{
76+
"status": "error",
77+
"reason": "threads_not_enabled",
78+
"errors": ["Threads not enabled for this chat"],
79+
"error_data": {"bot_id": "24348246-6791-4ac0-9d86-b948cd6a0e46"},
80+
},
81+
HTTPStatus.FORBIDDEN,
82+
ThreadCreationProhibitedError,
83+
),
84+
(
85+
{
86+
"status": "error",
87+
"reason": "bot_is_not_a_chat_member",
88+
"errors": ["This bot is not a chat member"],
89+
"error_data": {"bot_id": "24348246-6791-4ac0-9d86-b948cd6a0e46"},
90+
},
91+
HTTPStatus.FORBIDDEN,
92+
ThreadCreationProhibitedError,
93+
),
94+
(
95+
{
96+
"status": "error",
97+
"reason": "can_not_create_for_personal_chat",
98+
"errors": ["This is personal chat"],
99+
"error_data": {"bot_id": "24348246-6791-4ac0-9d86-b948cd6a0e46"},
100+
},
101+
HTTPStatus.FORBIDDEN,
102+
ThreadCreationProhibitedError,
103+
),
104+
(
105+
{
106+
"status": "error",
107+
"reason": "unsupported_event_type",
108+
"errors": ["This event type is unsupported"],
109+
"error_data": {"bot_id": "24348246-6791-4ac0-9d86-b948cd6a0e46"},
110+
},
111+
HTTPStatus.FORBIDDEN,
112+
ThreadCreationProhibitedError,
113+
),
114+
(
115+
{
116+
"status": "error",
117+
"reason": "unsupported_chat_type",
118+
"errors": ["This chat type is unsupported"],
119+
"error_data": {"bot_id": "24348246-6791-4ac0-9d86-b948cd6a0e46"},
120+
},
121+
HTTPStatus.FORBIDDEN,
122+
ThreadCreationProhibitedError,
123+
),
124+
(
125+
{
126+
"status": "error",
127+
"reason": "thread_already_created",
128+
"errors": ["Thread already created"],
129+
"error_data": {"bot_id": "24348246-6791-4ac0-9d86-b948cd6a0e46"},
130+
},
131+
HTTPStatus.FORBIDDEN,
132+
ThreadCreationProhibitedError,
133+
),
134+
(
135+
{
136+
"status": "error",
137+
"reason": "no_access_for_message",
138+
"errors": ["There is no access for this message"],
139+
"error_data": {"bot_id": "24348246-6791-4ac0-9d86-b948cd6a0e46"},
140+
},
141+
HTTPStatus.FORBIDDEN,
142+
ThreadCreationProhibitedError,
143+
),
144+
(
145+
{
146+
"status": "error",
147+
"reason": "event_in_stealth_mode",
148+
"errors": ["This event is in stealth mode"],
149+
"error_data": {"bot_id": "24348246-6791-4ac0-9d86-b948cd6a0e46"},
150+
},
151+
HTTPStatus.FORBIDDEN,
152+
ThreadCreationProhibitedError,
153+
),
154+
(
155+
{
156+
"status": "error",
157+
"reason": "event_already_deleted",
158+
"errors": ["This event already deleted"],
159+
"error_data": {"bot_id": "24348246-6791-4ac0-9d86-b948cd6a0e46"},
160+
},
161+
HTTPStatus.FORBIDDEN,
162+
ThreadCreationProhibitedError,
163+
),
164+
(
165+
{
166+
"status": "error",
167+
"reason": "event_not_found",
168+
"errors": ["Event not found"],
169+
"error_data": {"bot_id": "24348246-6791-4ac0-9d86-b948cd6a0e46"},
170+
},
171+
HTTPStatus.NOT_FOUND,
172+
ThreadCreationEventNotFoundError,
173+
),
174+
(
175+
{
176+
"status": "error",
177+
"reason": "|specified reason|",
178+
"errors": ["|specified errors|"],
179+
"error_data": {},
180+
},
181+
HTTPStatus.UNPROCESSABLE_ENTITY,
182+
ThreadCreationError,
183+
),
184+
),
185+
)
186+
async def test__create_thread__botx_error_raised(
187+
respx_mock: MockRouter,
188+
host: str,
189+
bot_id: UUID,
190+
bot_account: BotAccountWithSecret,
191+
return_json,
192+
response_status,
193+
expected_exc_type,
194+
) -> None:
195+
# - Arrange -
196+
sync_id = "21a9ec9e-f21f-4406-ac44-1a78d2ccf9e3"
197+
endpoint = respx_mock.post(
198+
f"https://{host}/{ENDPOINT}",
199+
headers={"Authorization": "Bearer token", "Content-Type": "application/json"},
200+
json={"sync_id": sync_id},
201+
).mock(return_value=httpx.Response(response_status, json=return_json))
202+
203+
built_bot = Bot(collectors=[HandlerCollector()], bot_accounts=[bot_account])
204+
205+
# - Act -
206+
async with lifespan_wrapper(built_bot) as bot:
207+
with pytest.raises(expected_exc_type) as exc:
208+
await bot.create_thread(
209+
bot_id=bot_id, sync_id=UUID(sync_id)
210+
)
211+
212+
# - Assert -
213+
assert endpoint.called
214+
assert return_json["reason"] in str(exc.value)

0 commit comments

Comments
 (0)