kakaotalk-bot 0.1.0__tar.gz

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.
@@ -0,0 +1,8 @@
1
+ __pycache__/
2
+ *.pyc
3
+ dist/
4
+ build/
5
+ *.egg-info/
6
+ .venv/
7
+ .mypy_cache/
8
+ .pytest_cache/
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 jade (punch5545)
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,140 @@
1
+ Metadata-Version: 2.5
2
+ Name: kakaotalk-bot
3
+ Version: 0.1.0
4
+ Summary: KakaoTalk Bot API client: REST API wrapper, Gateway (WebSocket) client and webhook helpers
5
+ Project-URL: Homepage, https://github.com/punch5545/KakaoTalk-Bot-Wrapper/tree/main/python#readme
6
+ Project-URL: Repository, https://github.com/punch5545/KakaoTalk-Bot-Wrapper
7
+ Project-URL: Issues, https://github.com/punch5545/KakaoTalk-Bot-Wrapper/issues
8
+ Project-URL: Documentation, https://developers.kakao.com/docs/ko/bot/common
9
+ Author: jade (punch5545)
10
+ License: MIT
11
+ License-File: LICENSE
12
+ Keywords: bot,chatbot,gateway,kakao,kakaotalk,websocket
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Typing :: Typed
21
+ Requires-Python: >=3.10
22
+ Requires-Dist: httpx>=0.27
23
+ Requires-Dist: websockets>=12.0
24
+ Provides-Extra: dev
25
+ Requires-Dist: mypy>=1.10; extra == 'dev'
26
+ Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
27
+ Requires-Dist: pytest>=8; extra == 'dev'
28
+ Description-Content-Type: text/markdown
29
+
30
+ # kakaotalk-bot (Python)
31
+
32
+ KakaoTalk Bot API client for Python 3.10+: REST API wrapper (async + sync), Gateway (WebSocket) client
33
+ and webhook helpers. Depends on `httpx` and `websockets`.
34
+
35
+ ```bash
36
+ pip install kakaotalk-bot
37
+ ```
38
+
39
+ ## REST API
40
+
41
+ ```python
42
+ from kakaotalk_bot import (
43
+ KakaoBotClient, KakaoBotSyncClient, KakaoBotApiError,
44
+ text_response, SkillResponseBuilder, basic_card, web_link_button, message_button, mention_tag,
45
+ )
46
+
47
+ async with KakaoBotClient(BOT_TOKEN) as client:
48
+ await client.send_callback(callback_token, text_response("처리가 완료되었습니다."))
49
+ await client.send_to_group(bot_group_key, text_response("안녕하세요!"))
50
+ await client.send_to_user(bot_user_key, text_response("안녕하세요!")) # channel chat only
51
+
52
+ commands = await client.get_commands()
53
+ await client.update_commands([{"buttonName": "도움말"}, {"buttonName": "상담원 연결", "description": "상담원과 연결합니다."}])
54
+ await client.update_guide("🤖 저와 함께 이런 것들을 할 수 있어요.")
55
+
56
+ response = (
57
+ SkillResponseBuilder()
58
+ .text(f"안녕하세요 {mention_tag('u1')}!")
59
+ .mention("u1", bot_user_key)
60
+ .add(basic_card("제목", "설명", {"imageUrl": "https://example.com/a.png"},
61
+ [web_link_button("열기", "https://example.com"), message_button("다시")]))
62
+ .quick_reply("도움말")
63
+ .build()
64
+ )
65
+ try:
66
+ await client.send_callback(callback_token, response)
67
+ except KakaoBotApiError as e:
68
+ if e.is_callback_token_error: # -881
69
+ ...
70
+
71
+ # Blocking variant with the same methods
72
+ with KakaoBotSyncClient(BOT_TOKEN) as client:
73
+ client.send_to_group(bot_group_key, text_response("hi"))
74
+ ```
75
+
76
+ Options: `KakaoBotClient(token, base_url=..., timeout=10.0, headers=..., http=httpx.AsyncClient(...))`.
77
+
78
+ ## Gateway (WebSocket)
79
+
80
+ ```python
81
+ import asyncio
82
+ from kakaotalk_bot import KakaoBotGateway, DispatchEvent
83
+
84
+ gateway = KakaoBotGateway(
85
+ BOT_TOKEN,
86
+ # url=..., client=..., auto_reconnect=True, initial_backoff=1.0, max_backoff=30.0,
87
+ # heartbeat_margin=5.0, handshake_timeout=15.0,
88
+ )
89
+
90
+ @gateway.on("ready")
91
+ def on_ready(ready):
92
+ print("bot", ready["bot"]["id"], "session", ready["session_id"])
93
+
94
+ @gateway.on("MESSAGE_CREATE")
95
+ async def on_message(event: DispatchEvent):
96
+ # event.data: {"id", "content", "timestamp", "isChannelChatroom", "botGroupKey", "botUserKey", "callbackToken"?}
97
+ if event.can_reply:
98
+ await event.reply_text(f"echo: {event.content}")
99
+
100
+ @gateway.on("error")
101
+ def on_error(exc):
102
+ print("gateway error:", exc) # fatal close codes: exc.fatal is True
103
+
104
+ asyncio.run(gateway.run()) # blocks; reconnects automatically
105
+ # or: task = await gateway.start() # background task, returns once READY
106
+ # await gateway.close()
107
+ ```
108
+
109
+ Events: `ready`, `resumed`, `dispatch`, `MESSAGE_CREATE`, `ENTRANCE`, `CHAT_JOIN`, `LEAVE`, `AD_REWARD`,
110
+ `close`, `reconnecting`, `heartbeat_timeout`, `error`. Listeners may be sync or async.
111
+
112
+ Session persistence: save `gateway.session` and call `gateway.restore_session(session_id, last_seq)`
113
+ before `run()` to RESUME across process restarts.
114
+
115
+ ## Webhook (FastAPI example)
116
+
117
+ ```python
118
+ from fastapi import FastAPI, Request, Response, BackgroundTasks
119
+ from kakaotalk_bot import KakaoBotClient, parse_webhook_event, verify_custom_header
120
+
121
+ client = KakaoBotClient(BOT_TOKEN)
122
+ app = FastAPI()
123
+
124
+ @app.post("/webhook")
125
+ async def webhook(request: Request, background: BackgroundTasks) -> Response:
126
+ if not verify_custom_header(request.headers.get("X-My-Secret"), WEBHOOK_SECRET):
127
+ return Response(status_code=401)
128
+ event = parse_webhook_event(await request.body(), client)
129
+ if event.type == "MESSAGE_CREATE" and event.can_reply:
130
+ background.add_task(event.reply_text, f"echo: {event.content}")
131
+ return Response(status_code=204) # within 1 second
132
+ ```
133
+
134
+ ## Development
135
+
136
+ ```bash
137
+ pip install -e ".[dev]"
138
+ pytest
139
+ mypy kakaotalk_bot
140
+ ```
@@ -0,0 +1,111 @@
1
+ # kakaotalk-bot (Python)
2
+
3
+ KakaoTalk Bot API client for Python 3.10+: REST API wrapper (async + sync), Gateway (WebSocket) client
4
+ and webhook helpers. Depends on `httpx` and `websockets`.
5
+
6
+ ```bash
7
+ pip install kakaotalk-bot
8
+ ```
9
+
10
+ ## REST API
11
+
12
+ ```python
13
+ from kakaotalk_bot import (
14
+ KakaoBotClient, KakaoBotSyncClient, KakaoBotApiError,
15
+ text_response, SkillResponseBuilder, basic_card, web_link_button, message_button, mention_tag,
16
+ )
17
+
18
+ async with KakaoBotClient(BOT_TOKEN) as client:
19
+ await client.send_callback(callback_token, text_response("처리가 완료되었습니다."))
20
+ await client.send_to_group(bot_group_key, text_response("안녕하세요!"))
21
+ await client.send_to_user(bot_user_key, text_response("안녕하세요!")) # channel chat only
22
+
23
+ commands = await client.get_commands()
24
+ await client.update_commands([{"buttonName": "도움말"}, {"buttonName": "상담원 연결", "description": "상담원과 연결합니다."}])
25
+ await client.update_guide("🤖 저와 함께 이런 것들을 할 수 있어요.")
26
+
27
+ response = (
28
+ SkillResponseBuilder()
29
+ .text(f"안녕하세요 {mention_tag('u1')}!")
30
+ .mention("u1", bot_user_key)
31
+ .add(basic_card("제목", "설명", {"imageUrl": "https://example.com/a.png"},
32
+ [web_link_button("열기", "https://example.com"), message_button("다시")]))
33
+ .quick_reply("도움말")
34
+ .build()
35
+ )
36
+ try:
37
+ await client.send_callback(callback_token, response)
38
+ except KakaoBotApiError as e:
39
+ if e.is_callback_token_error: # -881
40
+ ...
41
+
42
+ # Blocking variant with the same methods
43
+ with KakaoBotSyncClient(BOT_TOKEN) as client:
44
+ client.send_to_group(bot_group_key, text_response("hi"))
45
+ ```
46
+
47
+ Options: `KakaoBotClient(token, base_url=..., timeout=10.0, headers=..., http=httpx.AsyncClient(...))`.
48
+
49
+ ## Gateway (WebSocket)
50
+
51
+ ```python
52
+ import asyncio
53
+ from kakaotalk_bot import KakaoBotGateway, DispatchEvent
54
+
55
+ gateway = KakaoBotGateway(
56
+ BOT_TOKEN,
57
+ # url=..., client=..., auto_reconnect=True, initial_backoff=1.0, max_backoff=30.0,
58
+ # heartbeat_margin=5.0, handshake_timeout=15.0,
59
+ )
60
+
61
+ @gateway.on("ready")
62
+ def on_ready(ready):
63
+ print("bot", ready["bot"]["id"], "session", ready["session_id"])
64
+
65
+ @gateway.on("MESSAGE_CREATE")
66
+ async def on_message(event: DispatchEvent):
67
+ # event.data: {"id", "content", "timestamp", "isChannelChatroom", "botGroupKey", "botUserKey", "callbackToken"?}
68
+ if event.can_reply:
69
+ await event.reply_text(f"echo: {event.content}")
70
+
71
+ @gateway.on("error")
72
+ def on_error(exc):
73
+ print("gateway error:", exc) # fatal close codes: exc.fatal is True
74
+
75
+ asyncio.run(gateway.run()) # blocks; reconnects automatically
76
+ # or: task = await gateway.start() # background task, returns once READY
77
+ # await gateway.close()
78
+ ```
79
+
80
+ Events: `ready`, `resumed`, `dispatch`, `MESSAGE_CREATE`, `ENTRANCE`, `CHAT_JOIN`, `LEAVE`, `AD_REWARD`,
81
+ `close`, `reconnecting`, `heartbeat_timeout`, `error`. Listeners may be sync or async.
82
+
83
+ Session persistence: save `gateway.session` and call `gateway.restore_session(session_id, last_seq)`
84
+ before `run()` to RESUME across process restarts.
85
+
86
+ ## Webhook (FastAPI example)
87
+
88
+ ```python
89
+ from fastapi import FastAPI, Request, Response, BackgroundTasks
90
+ from kakaotalk_bot import KakaoBotClient, parse_webhook_event, verify_custom_header
91
+
92
+ client = KakaoBotClient(BOT_TOKEN)
93
+ app = FastAPI()
94
+
95
+ @app.post("/webhook")
96
+ async def webhook(request: Request, background: BackgroundTasks) -> Response:
97
+ if not verify_custom_header(request.headers.get("X-My-Secret"), WEBHOOK_SECRET):
98
+ return Response(status_code=401)
99
+ event = parse_webhook_event(await request.body(), client)
100
+ if event.type == "MESSAGE_CREATE" and event.can_reply:
101
+ background.add_task(event.reply_text, f"echo: {event.content}")
102
+ return Response(status_code=204) # within 1 second
103
+ ```
104
+
105
+ ## Development
106
+
107
+ ```bash
108
+ pip install -e ".[dev]"
109
+ pytest
110
+ mypy kakaotalk_bot
111
+ ```
@@ -0,0 +1,98 @@
1
+ """KakaoTalk Bot API client: REST API wrapper, Gateway (WebSocket) client and webhook helpers."""
2
+ from .client import DEFAULT_API_BASE_URL, MAX_COMMANDS, KakaoBotClient, KakaoBotSyncClient
3
+ from .errors import BOT_ERROR_DESCRIPTIONS, KakaoBotApiError, KakaoBotError, KakaoBotGatewayError
4
+ from .events import DispatchEvent, create_dispatch_event, is_dispatch, normalise_frame, parse_frame
5
+ from .gateway import DEFAULT_GATEWAY_URL, LIFECYCLE_EVENTS, KakaoBotGateway, describe_close_code
6
+ from .skill import (
7
+ MAX_MENTIONS,
8
+ MAX_OUTPUTS,
9
+ MAX_QUICK_REPLIES,
10
+ SkillResponseBuilder,
11
+ basic_card,
12
+ button,
13
+ carousel,
14
+ commerce_card,
15
+ guide_button,
16
+ image_response,
17
+ invite_button,
18
+ invite_member_button,
19
+ item_card,
20
+ list_card,
21
+ mention_button,
22
+ mention_tag,
23
+ mentions,
24
+ message_button,
25
+ operator_button,
26
+ phone_button,
27
+ quick_reply,
28
+ settings_button,
29
+ share_button,
30
+ simple_image,
31
+ simple_text,
32
+ skill_response,
33
+ text_card,
34
+ text_response,
35
+ web_link_button,
36
+ )
37
+ from .types import CLOSE_CODE_DESCRIPTIONS, EVENT_TYPES, FATAL_CLOSE_CODES, CloseCode, Opcode
38
+ from .webhook import parse_webhook_event, parse_webhook_payload, verify_custom_header
39
+
40
+ __version__ = "0.1.0"
41
+
42
+ __all__ = [
43
+ "__version__",
44
+ "KakaoBotClient",
45
+ "KakaoBotSyncClient",
46
+ "DEFAULT_API_BASE_URL",
47
+ "MAX_COMMANDS",
48
+ "KakaoBotError",
49
+ "KakaoBotApiError",
50
+ "KakaoBotGatewayError",
51
+ "BOT_ERROR_DESCRIPTIONS",
52
+ "DispatchEvent",
53
+ "create_dispatch_event",
54
+ "parse_frame",
55
+ "normalise_frame",
56
+ "is_dispatch",
57
+ "KakaoBotGateway",
58
+ "DEFAULT_GATEWAY_URL",
59
+ "LIFECYCLE_EVENTS",
60
+ "describe_close_code",
61
+ "SkillResponseBuilder",
62
+ "skill_response",
63
+ "text_response",
64
+ "image_response",
65
+ "simple_text",
66
+ "simple_image",
67
+ "text_card",
68
+ "basic_card",
69
+ "list_card",
70
+ "item_card",
71
+ "commerce_card",
72
+ "carousel",
73
+ "button",
74
+ "message_button",
75
+ "web_link_button",
76
+ "phone_button",
77
+ "share_button",
78
+ "operator_button",
79
+ "invite_button",
80
+ "mention_button",
81
+ "invite_member_button",
82
+ "settings_button",
83
+ "guide_button",
84
+ "quick_reply",
85
+ "mention_tag",
86
+ "mentions",
87
+ "MAX_OUTPUTS",
88
+ "MAX_QUICK_REPLIES",
89
+ "MAX_MENTIONS",
90
+ "Opcode",
91
+ "CloseCode",
92
+ "EVENT_TYPES",
93
+ "FATAL_CLOSE_CODES",
94
+ "CLOSE_CODE_DESCRIPTIONS",
95
+ "parse_webhook_payload",
96
+ "parse_webhook_event",
97
+ "verify_custom_header",
98
+ ]
@@ -0,0 +1,289 @@
1
+ """REST API client for the KakaoTalk Bot API (async and sync variants)."""
2
+ from __future__ import annotations
3
+
4
+ import json
5
+ from typing import Any, Mapping, Sequence
6
+
7
+ import httpx
8
+
9
+ from .errors import KakaoBotApiError
10
+ from .types import CommandsResponse, Guide, SendResult, SkillResponse, UtteranceRequest
11
+
12
+ DEFAULT_API_BASE_URL = "https://kapi.kakao.com"
13
+ MAX_COMMANDS = 30
14
+
15
+
16
+ class _BaseClient:
17
+ def __init__(
18
+ self,
19
+ bot_token: str,
20
+ *,
21
+ base_url: str = DEFAULT_API_BASE_URL,
22
+ timeout: float = 10.0,
23
+ headers: Mapping[str, str] | None = None,
24
+ ) -> None:
25
+ if not bot_token or not isinstance(bot_token, str):
26
+ raise TypeError("bot_token is required")
27
+ self._token = bot_token
28
+ self.base_url = base_url.rstrip("/")
29
+ self.timeout = timeout
30
+ self._extra_headers = dict(headers or {})
31
+
32
+ # -- request helpers ---------------------------------------------------
33
+
34
+ def _headers(self, extra: Mapping[str, str] | None = None, *, has_body: bool) -> dict[str, str]:
35
+ headers = {
36
+ **self._extra_headers,
37
+ "Authorization": f"KakaoAK {self._token}",
38
+ "Accept": "application/json",
39
+ }
40
+ if has_body:
41
+ headers["Content-Type"] = "application/json"
42
+ if extra:
43
+ headers.update(extra)
44
+ return headers
45
+
46
+ @staticmethod
47
+ def _encode(json_body: Any | None) -> bytes | None:
48
+ if json_body is None:
49
+ return None
50
+ return json.dumps(json_body, ensure_ascii=False).encode("utf-8")
51
+
52
+ @staticmethod
53
+ def _handle(response: httpx.Response) -> Any:
54
+ text = response.text
55
+ body: Any = None
56
+ if text:
57
+ try:
58
+ body = json.loads(text)
59
+ except ValueError:
60
+ body = None
61
+ if response.status_code >= 400:
62
+ raise KakaoBotApiError(response.status_code, body, text)
63
+ return body
64
+
65
+ @staticmethod
66
+ def _message_body(
67
+ skill_response: SkillResponse,
68
+ bot_group_key: str | None,
69
+ bot_user_key: str | None,
70
+ ) -> dict[str, Any]:
71
+ if bool(bot_group_key) == bool(bot_user_key):
72
+ raise TypeError("Exactly one of bot_group_key or bot_user_key must be provided")
73
+ body: dict[str, Any] = {}
74
+ if bot_group_key:
75
+ body["botGroupKey"] = bot_group_key
76
+ else:
77
+ body["botUserKey"] = bot_user_key
78
+ body["skillResponse"] = skill_response
79
+ return body
80
+
81
+ @staticmethod
82
+ def _validate_commands(utterances: Sequence[UtteranceRequest]) -> list[UtteranceRequest]:
83
+ items = list(utterances)
84
+ if len(items) > MAX_COMMANDS:
85
+ raise ValueError(f"At most {MAX_COMMANDS} commands can be registered")
86
+ return items
87
+
88
+
89
+ class KakaoBotClient(_BaseClient):
90
+ """Async REST client.
91
+
92
+ >>> async with KakaoBotClient(BOT_TOKEN) as client:
93
+ ... await client.send_callback(callback_token, text_response("안녕하세요!"))
94
+ """
95
+
96
+ def __init__(
97
+ self,
98
+ bot_token: str,
99
+ *,
100
+ base_url: str = DEFAULT_API_BASE_URL,
101
+ timeout: float = 10.0,
102
+ headers: Mapping[str, str] | None = None,
103
+ http: httpx.AsyncClient | None = None,
104
+ ) -> None:
105
+ super().__init__(bot_token, base_url=base_url, timeout=timeout, headers=headers)
106
+ self._owns_http = http is None
107
+ self._http = http or httpx.AsyncClient(timeout=timeout)
108
+
109
+ async def __aenter__(self) -> KakaoBotClient:
110
+ return self
111
+
112
+ async def __aexit__(self, *exc: object) -> None:
113
+ await self.aclose()
114
+
115
+ async def aclose(self) -> None:
116
+ if self._owns_http:
117
+ await self._http.aclose()
118
+
119
+ async def request(
120
+ self,
121
+ method: str,
122
+ path: str,
123
+ *,
124
+ json_body: Any | None = None,
125
+ headers: Mapping[str, str] | None = None,
126
+ ) -> Any:
127
+ """Low-level request; exposed for endpoints not yet wrapped."""
128
+ response = await self._http.request(
129
+ method,
130
+ self.base_url + path,
131
+ content=self._encode(json_body),
132
+ headers=self._headers(headers, has_body=json_body is not None),
133
+ timeout=self.timeout,
134
+ )
135
+ return self._handle(response)
136
+
137
+ # -- messages ----------------------------------------------------------
138
+
139
+ async def send_callback(self, callback_token: str, skill_response: SkillResponse) -> SendResult:
140
+ """POST /v1/bot/callback — reply to an event. Token is single-use and expires in 5 minutes."""
141
+ if not callback_token:
142
+ raise TypeError("callback_token is required")
143
+ result: SendResult = await self.request(
144
+ "POST",
145
+ "/v1/bot/callback",
146
+ json_body={"skillResponse": skill_response},
147
+ headers={"X-Bot-Callback-Token": callback_token},
148
+ )
149
+ return result
150
+
151
+ async def send_message(
152
+ self,
153
+ skill_response: SkillResponse,
154
+ *,
155
+ bot_group_key: str | None = None,
156
+ bot_user_key: str | None = None,
157
+ ) -> SendResult:
158
+ """POST /v1/bot/send_message — proactively message a room or a channel-chat user."""
159
+ result: SendResult = await self.request(
160
+ "POST",
161
+ "/v1/bot/send_message",
162
+ json_body=self._message_body(skill_response, bot_group_key, bot_user_key),
163
+ )
164
+ return result
165
+
166
+ async def send_to_group(self, bot_group_key: str, skill_response: SkillResponse) -> SendResult:
167
+ return await self.send_message(skill_response, bot_group_key=bot_group_key)
168
+
169
+ async def send_to_user(self, bot_user_key: str, skill_response: SkillResponse) -> SendResult:
170
+ return await self.send_message(skill_response, bot_user_key=bot_user_key)
171
+
172
+ # -- commands ----------------------------------------------------------
173
+
174
+ async def get_commands(self) -> CommandsResponse:
175
+ """GET /v1/bot/commands"""
176
+ result: CommandsResponse = await self.request("GET", "/v1/bot/commands")
177
+ return result
178
+
179
+ async def update_commands(self, utterances: Sequence[UtteranceRequest]) -> CommandsResponse:
180
+ """POST /v1/bot/commands/update — replaces the whole list (max 30)."""
181
+ result: CommandsResponse = await self.request(
182
+ "POST", "/v1/bot/commands/update", json_body={"utterances": self._validate_commands(utterances)}
183
+ )
184
+ return result
185
+
186
+ # -- guide -------------------------------------------------------------
187
+
188
+ async def get_guide(self) -> Guide:
189
+ """GET /v1/bot/guide"""
190
+ result: Guide = await self.request("GET", "/v1/bot/guide")
191
+ return result
192
+
193
+ async def update_guide(self, contents: str) -> Guide:
194
+ """POST /v1/bot/guide/update"""
195
+ result: Guide = await self.request("POST", "/v1/bot/guide/update", json_body={"contents": contents})
196
+ return result
197
+
198
+
199
+ class KakaoBotSyncClient(_BaseClient):
200
+ """Blocking REST client with the same surface as :class:`KakaoBotClient`."""
201
+
202
+ def __init__(
203
+ self,
204
+ bot_token: str,
205
+ *,
206
+ base_url: str = DEFAULT_API_BASE_URL,
207
+ timeout: float = 10.0,
208
+ headers: Mapping[str, str] | None = None,
209
+ http: httpx.Client | None = None,
210
+ ) -> None:
211
+ super().__init__(bot_token, base_url=base_url, timeout=timeout, headers=headers)
212
+ self._owns_http = http is None
213
+ self._http = http or httpx.Client(timeout=timeout)
214
+
215
+ def __enter__(self) -> KakaoBotSyncClient:
216
+ return self
217
+
218
+ def __exit__(self, *exc: object) -> None:
219
+ self.close()
220
+
221
+ def close(self) -> None:
222
+ if self._owns_http:
223
+ self._http.close()
224
+
225
+ def request(
226
+ self,
227
+ method: str,
228
+ path: str,
229
+ *,
230
+ json_body: Any | None = None,
231
+ headers: Mapping[str, str] | None = None,
232
+ ) -> Any:
233
+ response = self._http.request(
234
+ method,
235
+ self.base_url + path,
236
+ content=self._encode(json_body),
237
+ headers=self._headers(headers, has_body=json_body is not None),
238
+ timeout=self.timeout,
239
+ )
240
+ return self._handle(response)
241
+
242
+ def send_callback(self, callback_token: str, skill_response: SkillResponse) -> SendResult:
243
+ if not callback_token:
244
+ raise TypeError("callback_token is required")
245
+ result: SendResult = self.request(
246
+ "POST",
247
+ "/v1/bot/callback",
248
+ json_body={"skillResponse": skill_response},
249
+ headers={"X-Bot-Callback-Token": callback_token},
250
+ )
251
+ return result
252
+
253
+ def send_message(
254
+ self,
255
+ skill_response: SkillResponse,
256
+ *,
257
+ bot_group_key: str | None = None,
258
+ bot_user_key: str | None = None,
259
+ ) -> SendResult:
260
+ result: SendResult = self.request(
261
+ "POST",
262
+ "/v1/bot/send_message",
263
+ json_body=self._message_body(skill_response, bot_group_key, bot_user_key),
264
+ )
265
+ return result
266
+
267
+ def send_to_group(self, bot_group_key: str, skill_response: SkillResponse) -> SendResult:
268
+ return self.send_message(skill_response, bot_group_key=bot_group_key)
269
+
270
+ def send_to_user(self, bot_user_key: str, skill_response: SkillResponse) -> SendResult:
271
+ return self.send_message(skill_response, bot_user_key=bot_user_key)
272
+
273
+ def get_commands(self) -> CommandsResponse:
274
+ result: CommandsResponse = self.request("GET", "/v1/bot/commands")
275
+ return result
276
+
277
+ def update_commands(self, utterances: Sequence[UtteranceRequest]) -> CommandsResponse:
278
+ result: CommandsResponse = self.request(
279
+ "POST", "/v1/bot/commands/update", json_body={"utterances": self._validate_commands(utterances)}
280
+ )
281
+ return result
282
+
283
+ def get_guide(self) -> Guide:
284
+ result: Guide = self.request("GET", "/v1/bot/guide")
285
+ return result
286
+
287
+ def update_guide(self, contents: str) -> Guide:
288
+ result: Guide = self.request("POST", "/v1/bot/guide/update", json_body={"contents": contents})
289
+ return result