nestra-py 1.2.2__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.
- nestra_py-1.2.2/PKG-INFO +326 -0
- nestra_py-1.2.2/README.md +302 -0
- nestra_py-1.2.2/nestra/__init__.py +77 -0
- nestra_py-1.2.2/nestra/_ws.py +322 -0
- nestra_py-1.2.2/nestra/commands.py +247 -0
- nestra_py-1.2.2/nestra/embed.py +326 -0
- nestra_py-1.2.2/nestra/gateway.py +593 -0
- nestra_py-1.2.2/nestra/rest.py +1245 -0
- nestra_py-1.2.2/nestra/types.py +402 -0
- nestra_py-1.2.2/nestra_py.egg-info/PKG-INFO +326 -0
- nestra_py-1.2.2/nestra_py.egg-info/SOURCES.txt +24 -0
- nestra_py-1.2.2/nestra_py.egg-info/dependency_links.txt +1 -0
- nestra_py-1.2.2/nestra_py.egg-info/requires.txt +3 -0
- nestra_py-1.2.2/nestra_py.egg-info/top_level.txt +1 -0
- nestra_py-1.2.2/pyproject.toml +40 -0
- nestra_py-1.2.2/setup.cfg +4 -0
- nestra_py-1.2.2/tests/test_backoff.py +54 -0
- nestra_py-1.2.2/tests/test_bot_api_v122.py +315 -0
- nestra_py-1.2.2/tests/test_commands.py +181 -0
- nestra_py-1.2.2/tests/test_embeds.py +108 -0
- nestra_py-1.2.2/tests/test_gateway.py +133 -0
- nestra_py-1.2.2/tests/test_gateway_integration.py +196 -0
- nestra_py-1.2.2/tests/test_http_transport.py +136 -0
- nestra_py-1.2.2/tests/test_rest.py +331 -0
- nestra_py-1.2.2/tests/test_smoke.py +40 -0
- nestra_py-1.2.2/tests/test_ws.py +129 -0
nestra_py-1.2.2/PKG-INFO
ADDED
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: nestra-py
|
|
3
|
+
Version: 1.2.2
|
|
4
|
+
Summary: SDK Pythona dla API i Gateway Nestra (bot API 1.2.2: moderacja, role, reakcje, kanaly, zaproszenia, webhooki, DM, voice, komendy)
|
|
5
|
+
Author: Nestra
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://nestra.gg
|
|
8
|
+
Project-URL: Documentation, https://nestra.gg
|
|
9
|
+
Keywords: nestra,bot,chat,sdk,asyncio
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Framework :: AsyncIO
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
19
|
+
Classifier: Topic :: Communications :: Chat
|
|
20
|
+
Requires-Python: >=3.11
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
Provides-Extra: dev
|
|
23
|
+
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
24
|
+
|
|
25
|
+
# Nestra-PY
|
|
26
|
+
|
|
27
|
+
Oficjalne SDK Pythona do API i Gatewaya Nestra. Klient REST, klient WebSocket,
|
|
28
|
+
embedy, komendy prefiksowe i reconnect w jednym pakiecie.
|
|
29
|
+
|
|
30
|
+
## Cechy
|
|
31
|
+
|
|
32
|
+
- REST: gildie, kanały, wiadomości, członkowie, moderacja (kick/ban/timeout/role), reakcje, zaproszenia, webhooki, DM, voice, komendy, presence i uploady.
|
|
33
|
+
- Gateway: połączenie asyncio, IDENTIFY z `botToken`, heartbeat i reconnect z backoffem.
|
|
34
|
+
- Asynchroniczne handlery eventów: `READY`, `MESSAGE_CREATE`, `MESSAGE_UPDATE`, `MESSAGE_DELETE`, `GUILD_CREATE` i dowolne inne eventy.
|
|
35
|
+
- `EmbedBuilder` z limitami zgodnymi z API Nestra.
|
|
36
|
+
- `CommandHandler` dla komend prefiksowych z aliasami, pomocą i obsługą błędów.
|
|
37
|
+
- Rate limit queue czytająca nagłówki `X-RateLimit-*` i `Retry-After`.
|
|
38
|
+
- Zero zależności zewnętrznych w runtime. Wymagany jest tylko Python 3.11+.
|
|
39
|
+
|
|
40
|
+
## Instalacja
|
|
41
|
+
|
|
42
|
+
Z katalogu repozytorium:
|
|
43
|
+
|
|
44
|
+
```powershell
|
|
45
|
+
python -m pip install -e integrations/nestra-py
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Albo bez instalacji, przez `PYTHONPATH`:
|
|
49
|
+
|
|
50
|
+
```powershell
|
|
51
|
+
$env:PYTHONPATH = "integrations/nestra-py"
|
|
52
|
+
python examples/nestra-py-bot/bot.py
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Wymagania: Python 3.11 lub nowszy. SDK nie wymaga `websockets`, `aiohttp` ani
|
|
56
|
+
`requests`. Testy deweloperskie można uruchomić przez `pytest` albo wbudowany
|
|
57
|
+
`unittest`.
|
|
58
|
+
|
|
59
|
+
## Szybki start (REST)
|
|
60
|
+
|
|
61
|
+
```python
|
|
62
|
+
import asyncio
|
|
63
|
+
|
|
64
|
+
from nestra import EmbedBuilder, NestraREST
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
async def main() -> None:
|
|
68
|
+
rest = NestraREST.from_env()
|
|
69
|
+
me = await rest.get_current_user()
|
|
70
|
+
print(f"Zalogowany bot: {me.username}")
|
|
71
|
+
|
|
72
|
+
guilds = await rest.get_guilds()
|
|
73
|
+
if not guilds:
|
|
74
|
+
print("Bot nie jest członkiem żadnego serwera.")
|
|
75
|
+
return
|
|
76
|
+
|
|
77
|
+
detail = await rest.get_guild(guilds[0].id)
|
|
78
|
+
channel = detail.channel_by_name("ogolny")
|
|
79
|
+
if channel is None:
|
|
80
|
+
print("Brak kanału ogolny.")
|
|
81
|
+
return
|
|
82
|
+
|
|
83
|
+
embed = (
|
|
84
|
+
EmbedBuilder()
|
|
85
|
+
.set_title("Nestra-PY")
|
|
86
|
+
.set_description("Wiadomość wysłana przez SDK")
|
|
87
|
+
.set_color("blurple")
|
|
88
|
+
.set_footer("nestra.gg")
|
|
89
|
+
)
|
|
90
|
+
await rest.send_message(channel.id, embeds=embed)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
asyncio.run(main())
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
`NestraREST.from_env()` czyta `BOT_TOKEN` oraz opcjonalny `NESTRA_API_URL`
|
|
97
|
+
(domyślnie `http://localhost:4000`). Token bota ma prefiks `nestra_bot_` i jest
|
|
98
|
+
pokazywany w panelu Developer tylko raz. Bot musi być członkiem serwera: dodaje
|
|
99
|
+
go właściciel lub administrator przez panel albo bot dołącza kodem zaproszenia
|
|
100
|
+
(`join_guild`).
|
|
101
|
+
|
|
102
|
+
## Szybki start (Gateway)
|
|
103
|
+
|
|
104
|
+
```python
|
|
105
|
+
import asyncio
|
|
106
|
+
|
|
107
|
+
from nestra import GatewayClient, NestraREST
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
async def main() -> None:
|
|
111
|
+
rest = NestraREST.from_env()
|
|
112
|
+
bot = GatewayClient.from_env(rest=rest)
|
|
113
|
+
|
|
114
|
+
@bot.event("READY")
|
|
115
|
+
async def on_ready(data):
|
|
116
|
+
print(f"Połączono jako {bot.user.name if bot.user else '?'}")
|
|
117
|
+
|
|
118
|
+
@bot.event("MESSAGE_CREATE")
|
|
119
|
+
async def on_message(data):
|
|
120
|
+
message = data["message"]
|
|
121
|
+
if message["author"] and message["content"] == "!ping":
|
|
122
|
+
await rest.send_message(message["channelId"], "pong")
|
|
123
|
+
|
|
124
|
+
await bot.start()
|
|
125
|
+
await bot.wait_until_ready(timeout=30)
|
|
126
|
+
await bot.wait_until_closed()
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
asyncio.run(main())
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
`GatewayClient.from_env()` czyta `BOT_TOKEN`, opcjonalny `NESTRA_GATEWAY_URL`
|
|
133
|
+
(domyślnie `ws://localhost:4001/gateway`) oraz `NESTRA_GATEWAY_ORIGIN`
|
|
134
|
+
(domyślnie `http://localhost:5174`). Gateway Nestra wymaga nagłówka `Origin`
|
|
135
|
+
z listy CORS, dlatego bot nie jest przeglądarką, ale nadal wysyła ten nagłówek.
|
|
136
|
+
|
|
137
|
+
### Eventy
|
|
138
|
+
|
|
139
|
+
Handlery rejestruje się dekoratorem `@bot.event("NAZWA")`. Handler może być
|
|
140
|
+
funkcją synchroniczną albo `async`. SDK wywołuje je po kolei, więc wiadomości
|
|
141
|
+
zachowują kolejność. Błąd handlera trafia do `@bot.on_error` i nie zamyka
|
|
142
|
+
połączenia.
|
|
143
|
+
|
|
144
|
+
```python
|
|
145
|
+
@bot.on_error
|
|
146
|
+
async def on_error(error, event_name, data):
|
|
147
|
+
print(f"Błąd {event_name}: {error}")
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
`GUILD_CREATE` jest obsługiwany jak każdy inny event. Obecny Gateway wysyła
|
|
151
|
+
gildie w `READY` i eventy `GUILD_UPDATE`/`GUILD_DELETE`; rejestracja
|
|
152
|
+
`GUILD_CREATE` pozwala odebrać ten event, gdy pojawi się w protokole.
|
|
153
|
+
|
|
154
|
+
### Reconnect
|
|
155
|
+
|
|
156
|
+
Klient sam wysyła heartbeat na podstawie `HELLO`. Po zerwaniu połączenia bot
|
|
157
|
+
wysyła ponownie IDENTIFY, bo Gateway nie obsługuje RESUME dla tokenów botów.
|
|
158
|
+
Sesje użytkownika (`token_type="Bearer"`) mogą użyć RESUME. Opóźnienia rosną
|
|
159
|
+
wykładniczo (`ReconnectBackoff`) i mają jitter. Zamknięcia `4001` (błędny
|
|
160
|
+
token) oraz `4002` (błędny payload) są traktowane jako trwałe, bez reconnectu.
|
|
161
|
+
|
|
162
|
+
## Komendy prefiksowe
|
|
163
|
+
|
|
164
|
+
```python
|
|
165
|
+
from nestra import CommandContext, CommandHandler
|
|
166
|
+
|
|
167
|
+
commands = CommandHandler(prefix="!", rest=rest)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
@commands.command("ping", description="Odpowiada pongiem")
|
|
171
|
+
async def ping(ctx: CommandContext) -> None:
|
|
172
|
+
await ctx.reply("pong")
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
@bot.event("MESSAGE_CREATE")
|
|
176
|
+
async def on_message(data):
|
|
177
|
+
from nestra import Message
|
|
178
|
+
|
|
179
|
+
await commands.handle_message(Message.from_dict(data["message"]))
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
`parse_command("!ping teraz", "!")` zwraca `ParsedCommand(name="ping",
|
|
183
|
+
args="teraz", argv=["teraz"])`. Parser obsługuje cudzysłowy przez `shlex` i
|
|
184
|
+
nie wywala się na niezamkniętym cudzysłowie.
|
|
185
|
+
|
|
186
|
+
## Embedy
|
|
187
|
+
|
|
188
|
+
`EmbedBuilder` waliduje limity API: 256 znaków tytułu, 4096 opisu, 25 pól,
|
|
189
|
+
6000 znaków łącznie. Kolory można podawać jako `int`, hex (`#5865F2`) albo
|
|
190
|
+
nazwę (`blurple`, `green`, `red`).
|
|
191
|
+
|
|
192
|
+
```python
|
|
193
|
+
embed = (
|
|
194
|
+
EmbedBuilder()
|
|
195
|
+
.set_title("Status")
|
|
196
|
+
.set_description("Wszystkie systemy działają")
|
|
197
|
+
.set_color("#57F287")
|
|
198
|
+
.add_field("API", "ok", inline=True)
|
|
199
|
+
.add_field("Gateway", "ok", inline=True)
|
|
200
|
+
.set_timestamp()
|
|
201
|
+
)
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
Do API można przekazać pojedynczy builder, listę builderów albo zwykłe dict-y.
|
|
205
|
+
SDK wysyła wyłącznie adresy `http`/`https` w polach URL.
|
|
206
|
+
|
|
207
|
+
## API klienta
|
|
208
|
+
|
|
209
|
+
| Metoda | Opis |
|
|
210
|
+
|---|---|
|
|
211
|
+
| `get_guilds()` | lista gildii bota |
|
|
212
|
+
| `get_guild(guild_id)` | szczegóły gildii, kanały i role |
|
|
213
|
+
| `get_guild_channels(guild_id)` | kanały gildii |
|
|
214
|
+
| `get_members(guild_id, limit=50)` | członkowie gildii (do 1000) |
|
|
215
|
+
| `edit_guild(guild_id, ...)` | edycja nazwy/opisu/koloru gildii |
|
|
216
|
+
| `kick_member(guild_id, user_id, reason=...)` | wyrzucenie członka |
|
|
217
|
+
| `ban_member(...)` / `unban_member(...)` | ban i unban |
|
|
218
|
+
| `get_bans(guild_id)` | lista banów |
|
|
219
|
+
| `timeout_member(...)` | timeout członka |
|
|
220
|
+
| `add_role(...)` / `remove_role(...)` | role członka |
|
|
221
|
+
| `join_guild(invite_code)` | dodanie bota przez zaproszenie, zwraca ID gildii |
|
|
222
|
+
| `leave_guild(guild_id)` | opuszczenie gildii |
|
|
223
|
+
| `create_invite(...)` | zaproszenie do kanału |
|
|
224
|
+
| `get_invites(guild_id)` / `delete_invite(code)` | lista i usuwanie zaproszeń |
|
|
225
|
+
| `create_channel(guild_id, name, ...)` | nowy kanał |
|
|
226
|
+
| `update_channel(guild_id, channel_id, ...)` | edycja kanału |
|
|
227
|
+
| `delete_channel(guild_id, channel_id)` | usunięcie kanału |
|
|
228
|
+
| `get_messages(channel_id, limit=50, before=None)` | historia wiadomości |
|
|
229
|
+
| `send_message(channel_id, content, embeds=..., reply_to_id=...)` | wysyłka wiadomości |
|
|
230
|
+
| `edit_message(channel_id, message_id, content=..., embeds=...)` | edycja treści i/lub embedów |
|
|
231
|
+
| `edit_embed_message(channel_id, message_id, embeds)` | edycja z samym embedem |
|
|
232
|
+
| `delete_message(channel_id, message_id)` | usunięcie wiadomości bota |
|
|
233
|
+
| `add_reaction(...)` / `remove_reaction(...)` | reakcje bota |
|
|
234
|
+
| `get_current_user()` | konto bota (`/api/v1/bot/me`) |
|
|
235
|
+
| `get_user(user_id)` | profil użytkownika |
|
|
236
|
+
| `set_presence(status, ...)` | presence bota |
|
|
237
|
+
| `open_dm(user_id)` / `send_dm(user_id, ...)` | kanał DM i wiadomość |
|
|
238
|
+
| `join_voice(guild_id, channel_id)` / `leave_voice(guild_id)` | token LiveKit i wyjście |
|
|
239
|
+
| `get_commands()` / `register_commands(...)` / `delete_command(id)` | komendy aplikacji |
|
|
240
|
+
| `get_webhooks(guild_id)` | webhooki gildii |
|
|
241
|
+
| `create_webhook(...)` | nowy webhook |
|
|
242
|
+
| `update_webhook(...)` / `delete_webhook(guild_id, webhook_id)` | edycja i usunięcie |
|
|
243
|
+
| `execute_webhook(webhook_id, token, ...)` | wysyłka przez webhook, bez auth |
|
|
244
|
+
| `upload_attachment(channel_id, filename, data)` | upload pliku |
|
|
245
|
+
| `send_file(channel_id, filename, data, content=None)` | upload i wysyłka wiadomości |
|
|
246
|
+
|
|
247
|
+
Wszystkie metody bota korzystają z namespace `/api/v1/bot/...` (1.2.2). Kontrakt
|
|
248
|
+
rozszerza subagent bot-api równolegle; jeśli API zwróci 404 dla nowej trasy,
|
|
249
|
+
SDK zgłosi czytelny `NestraAPIError`, a pozostałe metody działają.
|
|
250
|
+
|
|
251
|
+
Wszystkie metody REST są asynchroniczne. `NestraREST.request()` pozwala wywołać
|
|
252
|
+
dowolny endpoint API bez oczekiwania na dodanie metody do SDK.
|
|
253
|
+
|
|
254
|
+
## Rate limity i błędy
|
|
255
|
+
|
|
256
|
+
Każde żądanie przechodzi przez `RateLimitQueue`. Po odpowiedzi `429` klient
|
|
257
|
+
czeka `Retry-After` (albo wartość z JSON) i ponawia żądanie, maksymalnie
|
|
258
|
+
`max_retries` razy. Błędy `5xx` i problemy sieciowe mają krótki backoff.
|
|
259
|
+
|
|
260
|
+
Błędy API są zgłaszane jako `NestraAPIError`:
|
|
261
|
+
|
|
262
|
+
```python
|
|
263
|
+
from nestra import NestraAPIError
|
|
264
|
+
|
|
265
|
+
try:
|
|
266
|
+
await rest.send_message(channel_id, "test")
|
|
267
|
+
except NestraAPIError as error:
|
|
268
|
+
print(error.status, error.code, error.message)
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
`error.status == 0` oznacza błąd sieci albo niepoprawną odpowiedź JSON.
|
|
272
|
+
|
|
273
|
+
## Bezpieczeństwo tokenu
|
|
274
|
+
|
|
275
|
+
- Token bota to sekret. Trzymaj go w zmiennej `BOT_TOKEN`, nie w kodzie ani w repozytorium.
|
|
276
|
+
- SDK nie loguje tokenu. `repr(NestraREST(...))` nie zawiera sekretu.
|
|
277
|
+
- Nie wysyłaj tokenu do przeglądarki ani do kodu klienta WWW.
|
|
278
|
+
- W produkcji używaj `https://` i `wss://`; połączenie z tokenem przez `http`/`ws` jest dopuszczalne tylko lokalnie.
|
|
279
|
+
- Przy wycieku tokenu unieważnij klucz w panelu deweloperskim i wygeneruj nowy.
|
|
280
|
+
- Bot powinien mieć tylko uprawnienia potrzebne do działania. Token nie zastępuje kontroli uprawnień po stronie API.
|
|
281
|
+
|
|
282
|
+
## Zgodność z bot auth
|
|
283
|
+
|
|
284
|
+
Bot auth jest wdrożony w API i Gatewayu:
|
|
285
|
+
|
|
286
|
+
- REST: `Authorization: Bot <token>` i trasy `/api/v1/bot/...`,
|
|
287
|
+
- Gateway: IDENTIFY przyjmuje dokładnie jedno pole, `botToken` albo `token`;
|
|
288
|
+
SDK wysyła `botToken` dla tokenu bota i `token` dla sesji użytkownika,
|
|
289
|
+
- READY bota zawiera `bot: true`, pustą obecność i listę gildii bota.
|
|
290
|
+
|
|
291
|
+
Nie mieszaj tokenu bota i JWT użytkownika w jednym kliencie. `token_type="Bearer"`
|
|
292
|
+
służy do sesji użytkownika, nie do kluczy `nstr_` ani tokenów `nestra_bot_`.
|
|
293
|
+
|
|
294
|
+
## Testy
|
|
295
|
+
|
|
296
|
+
Z katalogu pakietu:
|
|
297
|
+
|
|
298
|
+
```powershell
|
|
299
|
+
python -m unittest discover -s tests -v
|
|
300
|
+
python -m compileall -q nestra
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
Testy jednostkowe nie wymagają uruchomionego API. Test dymny uruchamia się
|
|
304
|
+
tylko z `NESTRA_SMOKE=1` i prawidłowym `BOT_TOKEN`:
|
|
305
|
+
|
|
306
|
+
```powershell
|
|
307
|
+
$env:NESTRA_SMOKE = "1"
|
|
308
|
+
python -m unittest tests.test_smoke -v
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
## Struktura
|
|
312
|
+
|
|
313
|
+
```text
|
|
314
|
+
integrations/nestra-py/
|
|
315
|
+
nestra/
|
|
316
|
+
__init__.py
|
|
317
|
+
rest.py # REST, rate limit, błędy
|
|
318
|
+
gateway.py # Gateway, heartbeat, reconnect, eventy
|
|
319
|
+
embed.py # EmbedBuilder
|
|
320
|
+
commands.py # CommandHandler
|
|
321
|
+
types.py # modele DTO
|
|
322
|
+
_ws.py # minimalny klient WebSocket
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
Przykładowy bot znajduje się w `examples/nestra-py-bot` w repozytorium Nestra.
|
|
326
|
+
|
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
# Nestra-PY
|
|
2
|
+
|
|
3
|
+
Oficjalne SDK Pythona do API i Gatewaya Nestra. Klient REST, klient WebSocket,
|
|
4
|
+
embedy, komendy prefiksowe i reconnect w jednym pakiecie.
|
|
5
|
+
|
|
6
|
+
## Cechy
|
|
7
|
+
|
|
8
|
+
- REST: gildie, kanały, wiadomości, członkowie, moderacja (kick/ban/timeout/role), reakcje, zaproszenia, webhooki, DM, voice, komendy, presence i uploady.
|
|
9
|
+
- Gateway: połączenie asyncio, IDENTIFY z `botToken`, heartbeat i reconnect z backoffem.
|
|
10
|
+
- Asynchroniczne handlery eventów: `READY`, `MESSAGE_CREATE`, `MESSAGE_UPDATE`, `MESSAGE_DELETE`, `GUILD_CREATE` i dowolne inne eventy.
|
|
11
|
+
- `EmbedBuilder` z limitami zgodnymi z API Nestra.
|
|
12
|
+
- `CommandHandler` dla komend prefiksowych z aliasami, pomocą i obsługą błędów.
|
|
13
|
+
- Rate limit queue czytająca nagłówki `X-RateLimit-*` i `Retry-After`.
|
|
14
|
+
- Zero zależności zewnętrznych w runtime. Wymagany jest tylko Python 3.11+.
|
|
15
|
+
|
|
16
|
+
## Instalacja
|
|
17
|
+
|
|
18
|
+
Z katalogu repozytorium:
|
|
19
|
+
|
|
20
|
+
```powershell
|
|
21
|
+
python -m pip install -e integrations/nestra-py
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Albo bez instalacji, przez `PYTHONPATH`:
|
|
25
|
+
|
|
26
|
+
```powershell
|
|
27
|
+
$env:PYTHONPATH = "integrations/nestra-py"
|
|
28
|
+
python examples/nestra-py-bot/bot.py
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Wymagania: Python 3.11 lub nowszy. SDK nie wymaga `websockets`, `aiohttp` ani
|
|
32
|
+
`requests`. Testy deweloperskie można uruchomić przez `pytest` albo wbudowany
|
|
33
|
+
`unittest`.
|
|
34
|
+
|
|
35
|
+
## Szybki start (REST)
|
|
36
|
+
|
|
37
|
+
```python
|
|
38
|
+
import asyncio
|
|
39
|
+
|
|
40
|
+
from nestra import EmbedBuilder, NestraREST
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
async def main() -> None:
|
|
44
|
+
rest = NestraREST.from_env()
|
|
45
|
+
me = await rest.get_current_user()
|
|
46
|
+
print(f"Zalogowany bot: {me.username}")
|
|
47
|
+
|
|
48
|
+
guilds = await rest.get_guilds()
|
|
49
|
+
if not guilds:
|
|
50
|
+
print("Bot nie jest członkiem żadnego serwera.")
|
|
51
|
+
return
|
|
52
|
+
|
|
53
|
+
detail = await rest.get_guild(guilds[0].id)
|
|
54
|
+
channel = detail.channel_by_name("ogolny")
|
|
55
|
+
if channel is None:
|
|
56
|
+
print("Brak kanału ogolny.")
|
|
57
|
+
return
|
|
58
|
+
|
|
59
|
+
embed = (
|
|
60
|
+
EmbedBuilder()
|
|
61
|
+
.set_title("Nestra-PY")
|
|
62
|
+
.set_description("Wiadomość wysłana przez SDK")
|
|
63
|
+
.set_color("blurple")
|
|
64
|
+
.set_footer("nestra.gg")
|
|
65
|
+
)
|
|
66
|
+
await rest.send_message(channel.id, embeds=embed)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
asyncio.run(main())
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
`NestraREST.from_env()` czyta `BOT_TOKEN` oraz opcjonalny `NESTRA_API_URL`
|
|
73
|
+
(domyślnie `http://localhost:4000`). Token bota ma prefiks `nestra_bot_` i jest
|
|
74
|
+
pokazywany w panelu Developer tylko raz. Bot musi być członkiem serwera: dodaje
|
|
75
|
+
go właściciel lub administrator przez panel albo bot dołącza kodem zaproszenia
|
|
76
|
+
(`join_guild`).
|
|
77
|
+
|
|
78
|
+
## Szybki start (Gateway)
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
import asyncio
|
|
82
|
+
|
|
83
|
+
from nestra import GatewayClient, NestraREST
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
async def main() -> None:
|
|
87
|
+
rest = NestraREST.from_env()
|
|
88
|
+
bot = GatewayClient.from_env(rest=rest)
|
|
89
|
+
|
|
90
|
+
@bot.event("READY")
|
|
91
|
+
async def on_ready(data):
|
|
92
|
+
print(f"Połączono jako {bot.user.name if bot.user else '?'}")
|
|
93
|
+
|
|
94
|
+
@bot.event("MESSAGE_CREATE")
|
|
95
|
+
async def on_message(data):
|
|
96
|
+
message = data["message"]
|
|
97
|
+
if message["author"] and message["content"] == "!ping":
|
|
98
|
+
await rest.send_message(message["channelId"], "pong")
|
|
99
|
+
|
|
100
|
+
await bot.start()
|
|
101
|
+
await bot.wait_until_ready(timeout=30)
|
|
102
|
+
await bot.wait_until_closed()
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
asyncio.run(main())
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
`GatewayClient.from_env()` czyta `BOT_TOKEN`, opcjonalny `NESTRA_GATEWAY_URL`
|
|
109
|
+
(domyślnie `ws://localhost:4001/gateway`) oraz `NESTRA_GATEWAY_ORIGIN`
|
|
110
|
+
(domyślnie `http://localhost:5174`). Gateway Nestra wymaga nagłówka `Origin`
|
|
111
|
+
z listy CORS, dlatego bot nie jest przeglądarką, ale nadal wysyła ten nagłówek.
|
|
112
|
+
|
|
113
|
+
### Eventy
|
|
114
|
+
|
|
115
|
+
Handlery rejestruje się dekoratorem `@bot.event("NAZWA")`. Handler może być
|
|
116
|
+
funkcją synchroniczną albo `async`. SDK wywołuje je po kolei, więc wiadomości
|
|
117
|
+
zachowują kolejność. Błąd handlera trafia do `@bot.on_error` i nie zamyka
|
|
118
|
+
połączenia.
|
|
119
|
+
|
|
120
|
+
```python
|
|
121
|
+
@bot.on_error
|
|
122
|
+
async def on_error(error, event_name, data):
|
|
123
|
+
print(f"Błąd {event_name}: {error}")
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
`GUILD_CREATE` jest obsługiwany jak każdy inny event. Obecny Gateway wysyła
|
|
127
|
+
gildie w `READY` i eventy `GUILD_UPDATE`/`GUILD_DELETE`; rejestracja
|
|
128
|
+
`GUILD_CREATE` pozwala odebrać ten event, gdy pojawi się w protokole.
|
|
129
|
+
|
|
130
|
+
### Reconnect
|
|
131
|
+
|
|
132
|
+
Klient sam wysyła heartbeat na podstawie `HELLO`. Po zerwaniu połączenia bot
|
|
133
|
+
wysyła ponownie IDENTIFY, bo Gateway nie obsługuje RESUME dla tokenów botów.
|
|
134
|
+
Sesje użytkownika (`token_type="Bearer"`) mogą użyć RESUME. Opóźnienia rosną
|
|
135
|
+
wykładniczo (`ReconnectBackoff`) i mają jitter. Zamknięcia `4001` (błędny
|
|
136
|
+
token) oraz `4002` (błędny payload) są traktowane jako trwałe, bez reconnectu.
|
|
137
|
+
|
|
138
|
+
## Komendy prefiksowe
|
|
139
|
+
|
|
140
|
+
```python
|
|
141
|
+
from nestra import CommandContext, CommandHandler
|
|
142
|
+
|
|
143
|
+
commands = CommandHandler(prefix="!", rest=rest)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
@commands.command("ping", description="Odpowiada pongiem")
|
|
147
|
+
async def ping(ctx: CommandContext) -> None:
|
|
148
|
+
await ctx.reply("pong")
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
@bot.event("MESSAGE_CREATE")
|
|
152
|
+
async def on_message(data):
|
|
153
|
+
from nestra import Message
|
|
154
|
+
|
|
155
|
+
await commands.handle_message(Message.from_dict(data["message"]))
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
`parse_command("!ping teraz", "!")` zwraca `ParsedCommand(name="ping",
|
|
159
|
+
args="teraz", argv=["teraz"])`. Parser obsługuje cudzysłowy przez `shlex` i
|
|
160
|
+
nie wywala się na niezamkniętym cudzysłowie.
|
|
161
|
+
|
|
162
|
+
## Embedy
|
|
163
|
+
|
|
164
|
+
`EmbedBuilder` waliduje limity API: 256 znaków tytułu, 4096 opisu, 25 pól,
|
|
165
|
+
6000 znaków łącznie. Kolory można podawać jako `int`, hex (`#5865F2`) albo
|
|
166
|
+
nazwę (`blurple`, `green`, `red`).
|
|
167
|
+
|
|
168
|
+
```python
|
|
169
|
+
embed = (
|
|
170
|
+
EmbedBuilder()
|
|
171
|
+
.set_title("Status")
|
|
172
|
+
.set_description("Wszystkie systemy działają")
|
|
173
|
+
.set_color("#57F287")
|
|
174
|
+
.add_field("API", "ok", inline=True)
|
|
175
|
+
.add_field("Gateway", "ok", inline=True)
|
|
176
|
+
.set_timestamp()
|
|
177
|
+
)
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
Do API można przekazać pojedynczy builder, listę builderów albo zwykłe dict-y.
|
|
181
|
+
SDK wysyła wyłącznie adresy `http`/`https` w polach URL.
|
|
182
|
+
|
|
183
|
+
## API klienta
|
|
184
|
+
|
|
185
|
+
| Metoda | Opis |
|
|
186
|
+
|---|---|
|
|
187
|
+
| `get_guilds()` | lista gildii bota |
|
|
188
|
+
| `get_guild(guild_id)` | szczegóły gildii, kanały i role |
|
|
189
|
+
| `get_guild_channels(guild_id)` | kanały gildii |
|
|
190
|
+
| `get_members(guild_id, limit=50)` | członkowie gildii (do 1000) |
|
|
191
|
+
| `edit_guild(guild_id, ...)` | edycja nazwy/opisu/koloru gildii |
|
|
192
|
+
| `kick_member(guild_id, user_id, reason=...)` | wyrzucenie członka |
|
|
193
|
+
| `ban_member(...)` / `unban_member(...)` | ban i unban |
|
|
194
|
+
| `get_bans(guild_id)` | lista banów |
|
|
195
|
+
| `timeout_member(...)` | timeout członka |
|
|
196
|
+
| `add_role(...)` / `remove_role(...)` | role członka |
|
|
197
|
+
| `join_guild(invite_code)` | dodanie bota przez zaproszenie, zwraca ID gildii |
|
|
198
|
+
| `leave_guild(guild_id)` | opuszczenie gildii |
|
|
199
|
+
| `create_invite(...)` | zaproszenie do kanału |
|
|
200
|
+
| `get_invites(guild_id)` / `delete_invite(code)` | lista i usuwanie zaproszeń |
|
|
201
|
+
| `create_channel(guild_id, name, ...)` | nowy kanał |
|
|
202
|
+
| `update_channel(guild_id, channel_id, ...)` | edycja kanału |
|
|
203
|
+
| `delete_channel(guild_id, channel_id)` | usunięcie kanału |
|
|
204
|
+
| `get_messages(channel_id, limit=50, before=None)` | historia wiadomości |
|
|
205
|
+
| `send_message(channel_id, content, embeds=..., reply_to_id=...)` | wysyłka wiadomości |
|
|
206
|
+
| `edit_message(channel_id, message_id, content=..., embeds=...)` | edycja treści i/lub embedów |
|
|
207
|
+
| `edit_embed_message(channel_id, message_id, embeds)` | edycja z samym embedem |
|
|
208
|
+
| `delete_message(channel_id, message_id)` | usunięcie wiadomości bota |
|
|
209
|
+
| `add_reaction(...)` / `remove_reaction(...)` | reakcje bota |
|
|
210
|
+
| `get_current_user()` | konto bota (`/api/v1/bot/me`) |
|
|
211
|
+
| `get_user(user_id)` | profil użytkownika |
|
|
212
|
+
| `set_presence(status, ...)` | presence bota |
|
|
213
|
+
| `open_dm(user_id)` / `send_dm(user_id, ...)` | kanał DM i wiadomość |
|
|
214
|
+
| `join_voice(guild_id, channel_id)` / `leave_voice(guild_id)` | token LiveKit i wyjście |
|
|
215
|
+
| `get_commands()` / `register_commands(...)` / `delete_command(id)` | komendy aplikacji |
|
|
216
|
+
| `get_webhooks(guild_id)` | webhooki gildii |
|
|
217
|
+
| `create_webhook(...)` | nowy webhook |
|
|
218
|
+
| `update_webhook(...)` / `delete_webhook(guild_id, webhook_id)` | edycja i usunięcie |
|
|
219
|
+
| `execute_webhook(webhook_id, token, ...)` | wysyłka przez webhook, bez auth |
|
|
220
|
+
| `upload_attachment(channel_id, filename, data)` | upload pliku |
|
|
221
|
+
| `send_file(channel_id, filename, data, content=None)` | upload i wysyłka wiadomości |
|
|
222
|
+
|
|
223
|
+
Wszystkie metody bota korzystają z namespace `/api/v1/bot/...` (1.2.2). Kontrakt
|
|
224
|
+
rozszerza subagent bot-api równolegle; jeśli API zwróci 404 dla nowej trasy,
|
|
225
|
+
SDK zgłosi czytelny `NestraAPIError`, a pozostałe metody działają.
|
|
226
|
+
|
|
227
|
+
Wszystkie metody REST są asynchroniczne. `NestraREST.request()` pozwala wywołać
|
|
228
|
+
dowolny endpoint API bez oczekiwania na dodanie metody do SDK.
|
|
229
|
+
|
|
230
|
+
## Rate limity i błędy
|
|
231
|
+
|
|
232
|
+
Każde żądanie przechodzi przez `RateLimitQueue`. Po odpowiedzi `429` klient
|
|
233
|
+
czeka `Retry-After` (albo wartość z JSON) i ponawia żądanie, maksymalnie
|
|
234
|
+
`max_retries` razy. Błędy `5xx` i problemy sieciowe mają krótki backoff.
|
|
235
|
+
|
|
236
|
+
Błędy API są zgłaszane jako `NestraAPIError`:
|
|
237
|
+
|
|
238
|
+
```python
|
|
239
|
+
from nestra import NestraAPIError
|
|
240
|
+
|
|
241
|
+
try:
|
|
242
|
+
await rest.send_message(channel_id, "test")
|
|
243
|
+
except NestraAPIError as error:
|
|
244
|
+
print(error.status, error.code, error.message)
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
`error.status == 0` oznacza błąd sieci albo niepoprawną odpowiedź JSON.
|
|
248
|
+
|
|
249
|
+
## Bezpieczeństwo tokenu
|
|
250
|
+
|
|
251
|
+
- Token bota to sekret. Trzymaj go w zmiennej `BOT_TOKEN`, nie w kodzie ani w repozytorium.
|
|
252
|
+
- SDK nie loguje tokenu. `repr(NestraREST(...))` nie zawiera sekretu.
|
|
253
|
+
- Nie wysyłaj tokenu do przeglądarki ani do kodu klienta WWW.
|
|
254
|
+
- W produkcji używaj `https://` i `wss://`; połączenie z tokenem przez `http`/`ws` jest dopuszczalne tylko lokalnie.
|
|
255
|
+
- Przy wycieku tokenu unieważnij klucz w panelu deweloperskim i wygeneruj nowy.
|
|
256
|
+
- Bot powinien mieć tylko uprawnienia potrzebne do działania. Token nie zastępuje kontroli uprawnień po stronie API.
|
|
257
|
+
|
|
258
|
+
## Zgodność z bot auth
|
|
259
|
+
|
|
260
|
+
Bot auth jest wdrożony w API i Gatewayu:
|
|
261
|
+
|
|
262
|
+
- REST: `Authorization: Bot <token>` i trasy `/api/v1/bot/...`,
|
|
263
|
+
- Gateway: IDENTIFY przyjmuje dokładnie jedno pole, `botToken` albo `token`;
|
|
264
|
+
SDK wysyła `botToken` dla tokenu bota i `token` dla sesji użytkownika,
|
|
265
|
+
- READY bota zawiera `bot: true`, pustą obecność i listę gildii bota.
|
|
266
|
+
|
|
267
|
+
Nie mieszaj tokenu bota i JWT użytkownika w jednym kliencie. `token_type="Bearer"`
|
|
268
|
+
służy do sesji użytkownika, nie do kluczy `nstr_` ani tokenów `nestra_bot_`.
|
|
269
|
+
|
|
270
|
+
## Testy
|
|
271
|
+
|
|
272
|
+
Z katalogu pakietu:
|
|
273
|
+
|
|
274
|
+
```powershell
|
|
275
|
+
python -m unittest discover -s tests -v
|
|
276
|
+
python -m compileall -q nestra
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
Testy jednostkowe nie wymagają uruchomionego API. Test dymny uruchamia się
|
|
280
|
+
tylko z `NESTRA_SMOKE=1` i prawidłowym `BOT_TOKEN`:
|
|
281
|
+
|
|
282
|
+
```powershell
|
|
283
|
+
$env:NESTRA_SMOKE = "1"
|
|
284
|
+
python -m unittest tests.test_smoke -v
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
## Struktura
|
|
288
|
+
|
|
289
|
+
```text
|
|
290
|
+
integrations/nestra-py/
|
|
291
|
+
nestra/
|
|
292
|
+
__init__.py
|
|
293
|
+
rest.py # REST, rate limit, błędy
|
|
294
|
+
gateway.py # Gateway, heartbeat, reconnect, eventy
|
|
295
|
+
embed.py # EmbedBuilder
|
|
296
|
+
commands.py # CommandHandler
|
|
297
|
+
types.py # modele DTO
|
|
298
|
+
_ws.py # minimalny klient WebSocket
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
Przykładowy bot znajduje się w `examples/nestra-py-bot` w repozytorium Nestra.
|
|
302
|
+
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""Nestra-PY - oficjalne SDK Pythona dla API i Gateway Nestra.
|
|
2
|
+
|
|
3
|
+
Pakiet nie ma zaleznosci zewnetrznych: REST korzysta z urllib, Gateway z
|
|
4
|
+
asyncio streams i wlasnej implementacji WebSocket.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from .commands import Command, CommandContext, CommandHandler, ParsedCommand, parse_command
|
|
10
|
+
from .embed import (
|
|
11
|
+
COLORS,
|
|
12
|
+
EMBED_MAX_COUNT,
|
|
13
|
+
EMBED_MAX_TOTAL_CHARS,
|
|
14
|
+
Embed,
|
|
15
|
+
EmbedBuilder,
|
|
16
|
+
parse_color,
|
|
17
|
+
total_characters,
|
|
18
|
+
validate_embeds,
|
|
19
|
+
)
|
|
20
|
+
from .gateway import GatewayClient, GatewayError, GatewayFatalError, ReconnectBackoff
|
|
21
|
+
from .rest import (
|
|
22
|
+
DEFAULT_API_URL,
|
|
23
|
+
NestraAPIError,
|
|
24
|
+
NestraREST,
|
|
25
|
+
RateLimitBucket,
|
|
26
|
+
RateLimitQueue,
|
|
27
|
+
)
|
|
28
|
+
from .types import (
|
|
29
|
+
Attachment,
|
|
30
|
+
Channel,
|
|
31
|
+
Guild,
|
|
32
|
+
GuildDetail,
|
|
33
|
+
Member,
|
|
34
|
+
Message,
|
|
35
|
+
MessagePage,
|
|
36
|
+
Role,
|
|
37
|
+
User,
|
|
38
|
+
Webhook,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
__version__ = "1.2.2"
|
|
42
|
+
|
|
43
|
+
__all__ = [
|
|
44
|
+
"Attachment",
|
|
45
|
+
"COLORS",
|
|
46
|
+
"Channel",
|
|
47
|
+
"Command",
|
|
48
|
+
"CommandContext",
|
|
49
|
+
"CommandHandler",
|
|
50
|
+
"DEFAULT_API_URL",
|
|
51
|
+
"EMBED_MAX_COUNT",
|
|
52
|
+
"EMBED_MAX_TOTAL_CHARS",
|
|
53
|
+
"Embed",
|
|
54
|
+
"EmbedBuilder",
|
|
55
|
+
"GatewayClient",
|
|
56
|
+
"GatewayError",
|
|
57
|
+
"GatewayFatalError",
|
|
58
|
+
"Guild",
|
|
59
|
+
"GuildDetail",
|
|
60
|
+
"Member",
|
|
61
|
+
"Message",
|
|
62
|
+
"MessagePage",
|
|
63
|
+
"NestraAPIError",
|
|
64
|
+
"NestraREST",
|
|
65
|
+
"ParsedCommand",
|
|
66
|
+
"RateLimitBucket",
|
|
67
|
+
"RateLimitQueue",
|
|
68
|
+
"ReconnectBackoff",
|
|
69
|
+
"Role",
|
|
70
|
+
"User",
|
|
71
|
+
"Webhook",
|
|
72
|
+
"__version__",
|
|
73
|
+
"parse_color",
|
|
74
|
+
"parse_command",
|
|
75
|
+
"total_characters",
|
|
76
|
+
"validate_embeds",
|
|
77
|
+
]
|