zynko 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.
- zynko-0.1.0/.gitignore +9 -0
- zynko-0.1.0/PKG-INFO +67 -0
- zynko-0.1.0/README.md +39 -0
- zynko-0.1.0/examples/dado.py +70 -0
- zynko-0.1.0/examples/musica.py +293 -0
- zynko-0.1.0/examples/ping.py +29 -0
- zynko-0.1.0/pyproject.toml +49 -0
- zynko-0.1.0/src/zynko/__init__.py +74 -0
- zynko-0.1.0/src/zynko/_events.py +123 -0
- zynko-0.1.0/src/zynko/bits.py +62 -0
- zynko-0.1.0/src/zynko/client.py +471 -0
- zynko-0.1.0/src/zynko/gateway.py +171 -0
- zynko-0.1.0/src/zynko/interaction.py +106 -0
- zynko-0.1.0/src/zynko/message.py +121 -0
- zynko-0.1.0/src/zynko/models.py +387 -0
- zynko-0.1.0/src/zynko/permissions.py +52 -0
- zynko-0.1.0/src/zynko/player.py +413 -0
- zynko-0.1.0/src/zynko/py.typed +0 -0
- zynko-0.1.0/src/zynko/rest.py +309 -0
- zynko-0.1.0/src/zynko/voice.py +298 -0
- zynko-0.1.0/tests/test_bits.py +25 -0
- zynko-0.1.0/tests/test_client.py +251 -0
- zynko-0.1.0/tests/test_permissions.py +89 -0
- zynko-0.1.0/tests/test_player.py +185 -0
- zynko-0.1.0/tests/test_voice.py +180 -0
zynko-0.1.0/.gitignore
ADDED
zynko-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: zynko
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Biblioteca oficial para bots do Zynko: API, gateway e voz.
|
|
5
|
+
Project-URL: Documentation, https://docs.zynko.org/bots/python
|
|
6
|
+
Project-URL: Homepage, https://zynko.org
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
Keywords: asyncio,bot,chat,voice,zynko
|
|
9
|
+
Classifier: Development Status :: 4 - Beta
|
|
10
|
+
Classifier: Framework :: AsyncIO
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
18
|
+
Classifier: Typing :: Typed
|
|
19
|
+
Requires-Python: >=3.10
|
|
20
|
+
Requires-Dist: aiohttp<4,>=3.11
|
|
21
|
+
Provides-Extra: dev
|
|
22
|
+
Requires-Dist: mypy>=1.13; extra == 'dev'
|
|
23
|
+
Requires-Dist: pytest-asyncio>=0.24; extra == 'dev'
|
|
24
|
+
Requires-Dist: pytest>=8.3; extra == 'dev'
|
|
25
|
+
Provides-Extra: voice
|
|
26
|
+
Requires-Dist: livekit==1.2.1; extra == 'voice'
|
|
27
|
+
Description-Content-Type: text/markdown
|
|
28
|
+
|
|
29
|
+
# zynko
|
|
30
|
+
|
|
31
|
+
A biblioteca oficial para bots do [Zynko](https://zynko.org) em Python: gateway com
|
|
32
|
+
reconexão, cache de servidores, canais e membros, a API inteira, comandos com "/", cartões e
|
|
33
|
+
botões, e voz (tocar e ouvir áudio nas salas). Feita com asyncio.
|
|
34
|
+
|
|
35
|
+
```sh
|
|
36
|
+
pip install zynko
|
|
37
|
+
pip install "zynko[voice]" # com voz
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
```python
|
|
41
|
+
import os
|
|
42
|
+
import zynko
|
|
43
|
+
|
|
44
|
+
client = zynko.Client(intents=zynko.Intents.GUILDS | zynko.Intents.MESSAGES)
|
|
45
|
+
|
|
46
|
+
@client.on("message_create")
|
|
47
|
+
async def ping(msg: zynko.Message):
|
|
48
|
+
if not msg.mine and msg.content == "!ping":
|
|
49
|
+
await msg.reply("pong")
|
|
50
|
+
|
|
51
|
+
client.run(os.environ["ZYNKO_TOKEN"])
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Voz:
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
voice = await client.join_voice(channel_id)
|
|
58
|
+
voice.player.enqueue("https://exemplo.com/musica.mp3")
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Python 3.10 ou mais novo. Para tocar arquivos e links, o ffmpeg precisa estar instalado.
|
|
62
|
+
Exemplos em [`examples/`](examples): `ping.py`, `dado.py` (comandos, cartão e botões) e
|
|
63
|
+
`musica.py`.
|
|
64
|
+
|
|
65
|
+
Documentação: https://docs.zynko.org/bots/python · Crie seu bot: https://dev.zynko.org
|
|
66
|
+
|
|
67
|
+
Licença MIT.
|
zynko-0.1.0/README.md
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# zynko
|
|
2
|
+
|
|
3
|
+
A biblioteca oficial para bots do [Zynko](https://zynko.org) em Python: gateway com
|
|
4
|
+
reconexão, cache de servidores, canais e membros, a API inteira, comandos com "/", cartões e
|
|
5
|
+
botões, e voz (tocar e ouvir áudio nas salas). Feita com asyncio.
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
pip install zynko
|
|
9
|
+
pip install "zynko[voice]" # com voz
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
```python
|
|
13
|
+
import os
|
|
14
|
+
import zynko
|
|
15
|
+
|
|
16
|
+
client = zynko.Client(intents=zynko.Intents.GUILDS | zynko.Intents.MESSAGES)
|
|
17
|
+
|
|
18
|
+
@client.on("message_create")
|
|
19
|
+
async def ping(msg: zynko.Message):
|
|
20
|
+
if not msg.mine and msg.content == "!ping":
|
|
21
|
+
await msg.reply("pong")
|
|
22
|
+
|
|
23
|
+
client.run(os.environ["ZYNKO_TOKEN"])
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Voz:
|
|
27
|
+
|
|
28
|
+
```python
|
|
29
|
+
voice = await client.join_voice(channel_id)
|
|
30
|
+
voice.player.enqueue("https://exemplo.com/musica.mp3")
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Python 3.10 ou mais novo. Para tocar arquivos e links, o ffmpeg precisa estar instalado.
|
|
34
|
+
Exemplos em [`examples/`](examples): `ping.py`, `dado.py` (comandos, cartão e botões) e
|
|
35
|
+
`musica.py`.
|
|
36
|
+
|
|
37
|
+
Documentação: https://docs.zynko.org/bots/python · Crie seu bot: https://dev.zynko.org
|
|
38
|
+
|
|
39
|
+
Licença MIT.
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""Slash commands, a card and buttons: /dado rolls a die and offers to roll again.
|
|
2
|
+
|
|
3
|
+
ZYNKO_TOKEN=bot_... python dado.py
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import os
|
|
7
|
+
import random
|
|
8
|
+
|
|
9
|
+
import zynko
|
|
10
|
+
|
|
11
|
+
LAVENDER = 0xA594F9
|
|
12
|
+
|
|
13
|
+
client = zynko.Client(intents=zynko.Intents.GUILDS, api=os.environ.get("ZYNKO_API"))
|
|
14
|
+
|
|
15
|
+
COMMANDS: list[zynko.Command] = [
|
|
16
|
+
{
|
|
17
|
+
"name": "dado",
|
|
18
|
+
"description": "Rola um dado",
|
|
19
|
+
"options": [{"name": "lados", "description": "Quantos lados", "type": "integer", "min_value": 2, "max_value": 100}],
|
|
20
|
+
},
|
|
21
|
+
{"name": "ping", "description": "Responde pong"},
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def roll(sides: int, who: str) -> tuple[list[zynko.Card], zynko.Components]:
|
|
26
|
+
"""The card with a roll, and the buttons under it."""
|
|
27
|
+
card: zynko.Card = {
|
|
28
|
+
"title": f"🎲 {random.randint(1, sides)}",
|
|
29
|
+
"description": f"Dado de {sides} lados, rolado por **{who}**.",
|
|
30
|
+
"color": LAVENDER,
|
|
31
|
+
}
|
|
32
|
+
buttons: zynko.Components = [
|
|
33
|
+
[
|
|
34
|
+
{"style": "primary", "emoji": "🎲", "label": "De novo", "custom_id": f"dado:{sides}"},
|
|
35
|
+
{"style": "secondary", "label": "Pronto", "custom_id": "dado:fim"},
|
|
36
|
+
]
|
|
37
|
+
]
|
|
38
|
+
return [card], buttons
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@client.event
|
|
42
|
+
async def on_ready(user: zynko.User) -> None:
|
|
43
|
+
await client.set_commands(COMMANDS)
|
|
44
|
+
print(f"{user.display_name} pronto, com {len(COMMANDS)} comandos")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@client.event
|
|
48
|
+
async def on_interaction(i: zynko.Interaction) -> None:
|
|
49
|
+
who = i.member.name if i.member else i.user.display_name
|
|
50
|
+
if i.type == "command":
|
|
51
|
+
if i.command == "ping":
|
|
52
|
+
await i.reply("pong", ephemeral=True)
|
|
53
|
+
elif i.command == "dado":
|
|
54
|
+
cards, buttons = roll(int(i.option("lados", 6)), who)
|
|
55
|
+
await i.reply(cards=cards, components=buttons)
|
|
56
|
+
return
|
|
57
|
+
# A click on one of the buttons.
|
|
58
|
+
if i.custom_id == "dado:fim":
|
|
59
|
+
await i.update(components=[])
|
|
60
|
+
elif i.custom_id and i.custom_id.startswith("dado:"):
|
|
61
|
+
cards, buttons = roll(int(i.custom_id.split(":")[1]), who)
|
|
62
|
+
await i.update(cards=cards, components=buttons)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@client.event
|
|
66
|
+
async def on_error(error: Exception) -> None:
|
|
67
|
+
print("erro:", error)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
client.run(os.environ["ZYNKO_TOKEN"])
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
"""A small music bot: joins your voice room and plays links (mp3, ogg, radio streams, anything
|
|
2
|
+
ffmpeg opens), with a queue. Use it with "/" commands (/tocar, /fila...) or the "!" ones
|
|
3
|
+
(!tocar <link>...); what is playing shows as a card with buttons.
|
|
4
|
+
|
|
5
|
+
pip install "zynko[voice]"
|
|
6
|
+
ZYNKO_TOKEN=bot_... python musica.py
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import asyncio
|
|
12
|
+
import os
|
|
13
|
+
import re
|
|
14
|
+
from collections.abc import Awaitable, Callable
|
|
15
|
+
from dataclasses import dataclass
|
|
16
|
+
from typing import Any
|
|
17
|
+
from urllib.parse import unquote, urlparse
|
|
18
|
+
|
|
19
|
+
import zynko
|
|
20
|
+
from zynko import Intents, Permissions
|
|
21
|
+
|
|
22
|
+
PREFIX = os.environ.get("PREFIX", "!")
|
|
23
|
+
LAVENDER = 0xA594F9
|
|
24
|
+
|
|
25
|
+
client = zynko.Client(
|
|
26
|
+
intents=Intents.GUILDS | Intents.MESSAGES | Intents.VOICE_STATES,
|
|
27
|
+
api=os.environ.get("ZYNKO_API"),
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass
|
|
32
|
+
class Song:
|
|
33
|
+
url: str
|
|
34
|
+
title: str
|
|
35
|
+
#: Who asked for it, and where to talk about it.
|
|
36
|
+
by: str
|
|
37
|
+
channel_id: str
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass
|
|
41
|
+
class Ctx:
|
|
42
|
+
"""Where a command came from: a "!" message or a "/" command, answered the same way."""
|
|
43
|
+
|
|
44
|
+
user_id: str
|
|
45
|
+
name: str
|
|
46
|
+
guild_id: str | None
|
|
47
|
+
channel_id: str
|
|
48
|
+
#: Answers; ``quiet`` ones only the person sees, when the command came by "/".
|
|
49
|
+
say: Callable[..., Awaitable[Any]]
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def title_of(url: str) -> str:
|
|
53
|
+
"""A title for the queue: the link's last part."""
|
|
54
|
+
u = urlparse(url)
|
|
55
|
+
last = [p for p in u.path.split("/") if p]
|
|
56
|
+
return unquote(last[-1]) if last else u.netloc or url
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def now_playing() -> dict[str, Any]:
|
|
60
|
+
"""The "Tocando agora" card, with the queue's next ones and the buttons."""
|
|
61
|
+
p = client.voice.player if client.voice else None
|
|
62
|
+
song: Song | None = p.current.meta if p and p.current else None
|
|
63
|
+
if not p or not song:
|
|
64
|
+
return {"cards": [{"title": "Nada tocando", "color": LAVENDER}], "components": []}
|
|
65
|
+
nxt = [f"{n}. {t.meta.title if t.meta else '?'}" for n, t in enumerate(p.queue[:5], 1)]
|
|
66
|
+
fields: list[zynko.CardField] = [
|
|
67
|
+
{"name": "Pedido por", "value": song.by, "inline": True},
|
|
68
|
+
{"name": "Volume", "value": f"{round(p.volume * 100)}%", "inline": True},
|
|
69
|
+
]
|
|
70
|
+
if nxt:
|
|
71
|
+
fields.append({"name": f"Próximas ({len(p.queue)})", "value": "\n".join(nxt)})
|
|
72
|
+
card: zynko.Card = {
|
|
73
|
+
"author": {"name": "Pausado" if p.paused else "Tocando agora"},
|
|
74
|
+
"title": song.title,
|
|
75
|
+
"color": LAVENDER,
|
|
76
|
+
"fields": fields,
|
|
77
|
+
}
|
|
78
|
+
if song.url:
|
|
79
|
+
card["url"] = song.url
|
|
80
|
+
buttons: zynko.Components = [
|
|
81
|
+
[
|
|
82
|
+
{
|
|
83
|
+
"style": "secondary",
|
|
84
|
+
"emoji": "▶️" if p.paused else "⏸️",
|
|
85
|
+
"label": "Continuar" if p.paused else "Pausar",
|
|
86
|
+
"custom_id": "pausar",
|
|
87
|
+
},
|
|
88
|
+
{"style": "primary", "emoji": "⏭️", "label": "Pular", "custom_id": "pular"},
|
|
89
|
+
{"style": "danger", "emoji": "⏹️", "label": "Parar", "custom_id": "parar"},
|
|
90
|
+
]
|
|
91
|
+
]
|
|
92
|
+
return {"cards": [card], "components": buttons}
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
async def voice_for(ctx: Ctx) -> Any:
|
|
96
|
+
"""Joins the author's voice room, or says why not."""
|
|
97
|
+
state = client.voice_states.get(ctx.user_id)
|
|
98
|
+
where = state.channel_id if state else None
|
|
99
|
+
room = client.channels.get(where) if where else None
|
|
100
|
+
if not where or not room or room.guild_id != ctx.guild_id:
|
|
101
|
+
await ctx.say("Entre numa sala de voz deste servidor primeiro.", True)
|
|
102
|
+
return None
|
|
103
|
+
if client.voice and client.voice.channel_id == where:
|
|
104
|
+
return client.voice
|
|
105
|
+
assert client.user is not None
|
|
106
|
+
if not client.can(where, client.user.id, Permissions.CONNECT | Permissions.SPEAK):
|
|
107
|
+
await ctx.say("Não tenho permissão para entrar e falar nessa sala.", True)
|
|
108
|
+
return None
|
|
109
|
+
voice = await client.join_voice(where)
|
|
110
|
+
|
|
111
|
+
@voice.player.on("start")
|
|
112
|
+
async def started(track: zynko.Track[Song]) -> None:
|
|
113
|
+
if track.meta:
|
|
114
|
+
await client.send(track.meta.channel_id, **now_playing())
|
|
115
|
+
|
|
116
|
+
@voice.player.on("error")
|
|
117
|
+
async def failed(error: Exception, track: zynko.Track[Song]) -> None:
|
|
118
|
+
if track.meta:
|
|
119
|
+
await client.send(track.meta.channel_id, f"Não consegui tocar **{track.meta.title}**: {error}")
|
|
120
|
+
|
|
121
|
+
voice.on("disconnect", lambda reason: print(f"saiu da voz: {reason}"))
|
|
122
|
+
return voice
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
async def tocar(ctx: Ctx, arg: str) -> Any:
|
|
126
|
+
if not re.fullmatch(r"https?://\S+", arg, re.I):
|
|
127
|
+
return await ctx.say(f"Use: {PREFIX}tocar <link de áudio>", True)
|
|
128
|
+
voice = await voice_for(ctx)
|
|
129
|
+
if not voice:
|
|
130
|
+
return None
|
|
131
|
+
song = Song(arg, title_of(arg), ctx.name, ctx.channel_id)
|
|
132
|
+
playing = voice.player.current is not None
|
|
133
|
+
voice.player.enqueue(arg, song)
|
|
134
|
+
if playing:
|
|
135
|
+
return await ctx.say(f"Na fila ({len(voice.player.queue)}): **{song.title}**")
|
|
136
|
+
return await ctx.say(f"Vai tocar: **{song.title}**", True)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
async def teste(ctx: Ctx, arg: str) -> Any:
|
|
140
|
+
voice = await voice_for(ctx)
|
|
141
|
+
if voice:
|
|
142
|
+
voice.player.play(zynko.tone(440, 3), Song("", "um lá de 440 Hz", ctx.name, ctx.channel_id))
|
|
143
|
+
return await ctx.say("Tocando um tom de 3 segundos.", True)
|
|
144
|
+
return None
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
async def pular(ctx: Ctx, arg: str) -> Any:
|
|
148
|
+
if not client.voice or not client.voice.player.current:
|
|
149
|
+
return await ctx.say("Nada tocando.", True)
|
|
150
|
+
client.voice.player.skip()
|
|
151
|
+
return await ctx.say("⏭️ Pulei.", True)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
async def parar(ctx: Ctx, arg: str) -> Any:
|
|
155
|
+
if client.voice:
|
|
156
|
+
client.voice.player.stop()
|
|
157
|
+
return await ctx.say("⏹️ Parei e esvaziei a fila.", True)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
async def pausar(ctx: Ctx, arg: str) -> Any:
|
|
161
|
+
if client.voice:
|
|
162
|
+
client.voice.player.pause()
|
|
163
|
+
return await ctx.say("⏸️ Pausado.", True)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
async def continuar(ctx: Ctx, arg: str) -> Any:
|
|
167
|
+
if client.voice:
|
|
168
|
+
client.voice.player.resume()
|
|
169
|
+
return await ctx.say("▶️ Continuando.", True)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
async def fila(ctx: Ctx, arg: str) -> Any:
|
|
173
|
+
return await ctx.say(now_playing())
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
async def volume(ctx: Ctx, arg: str) -> Any:
|
|
177
|
+
if not client.voice:
|
|
178
|
+
return await ctx.say("Não estou em nenhuma sala.", True)
|
|
179
|
+
p = client.voice.player
|
|
180
|
+
try:
|
|
181
|
+
v = float(arg)
|
|
182
|
+
except ValueError:
|
|
183
|
+
v = -1
|
|
184
|
+
if not 0 <= v <= 200:
|
|
185
|
+
return await ctx.say(f"Volume atual: {round(p.volume * 100)}%.", True)
|
|
186
|
+
p.volume = v / 100
|
|
187
|
+
return await ctx.say(f"🔊 Volume em {v:g}%.", True)
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
async def sair(ctx: Ctx, arg: str) -> Any:
|
|
191
|
+
if client.voice:
|
|
192
|
+
await client.voice.leave()
|
|
193
|
+
return await ctx.say("👋 Saí da sala.", True)
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
COMMANDS: dict[str, Callable[[Ctx, str], Awaitable[Any]]] = {
|
|
197
|
+
"tocar": tocar,
|
|
198
|
+
"teste": teste,
|
|
199
|
+
"pular": pular,
|
|
200
|
+
"parar": parar,
|
|
201
|
+
"pausar": pausar,
|
|
202
|
+
"continuar": continuar,
|
|
203
|
+
"fila": fila,
|
|
204
|
+
"volume": volume,
|
|
205
|
+
"sair": sair,
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
#: The "/" commands, as the apps show them.
|
|
209
|
+
SLASH: list[zynko.Command] = [
|
|
210
|
+
{
|
|
211
|
+
"name": "tocar",
|
|
212
|
+
"description": "Toca um link, ou põe na fila",
|
|
213
|
+
"options": [{"name": "link", "description": "Link de áudio (mp3, ogg, rádio...)", "type": "string", "required": True}],
|
|
214
|
+
},
|
|
215
|
+
{"name": "pular", "description": "Vai para a próxima da fila"},
|
|
216
|
+
{"name": "parar", "description": "Para e esvazia a fila"},
|
|
217
|
+
{"name": "pausar", "description": "Pausa a música"},
|
|
218
|
+
{"name": "continuar", "description": "Continua a música pausada"},
|
|
219
|
+
{"name": "fila", "description": "O que está tocando e as próximas"},
|
|
220
|
+
{
|
|
221
|
+
"name": "volume",
|
|
222
|
+
"description": "Muda o volume",
|
|
223
|
+
"options": [{"name": "porcentagem", "description": "0 a 200", "type": "integer", "min_value": 0, "max_value": 200}],
|
|
224
|
+
},
|
|
225
|
+
{"name": "sair", "description": "Sai da sala de voz"},
|
|
226
|
+
{"name": "teste", "description": "Toca um tom de 3 segundos"},
|
|
227
|
+
]
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
@client.event
|
|
231
|
+
async def on_message_create(msg: zynko.Message) -> None:
|
|
232
|
+
if msg.mine or msg.author.bot or not msg.guild_id or not msg.content.startswith(PREFIX):
|
|
233
|
+
return
|
|
234
|
+
name, _, arg = msg.content[len(PREFIX) :].strip().partition(" ")
|
|
235
|
+
command = COMMANDS.get(name.lower())
|
|
236
|
+
if not command:
|
|
237
|
+
return
|
|
238
|
+
member = msg.member
|
|
239
|
+
|
|
240
|
+
async def say(answer: str | dict[str, Any], quiet: bool = False) -> Any:
|
|
241
|
+
return await (msg.reply(answer) if isinstance(answer, str) else msg.reply(**answer))
|
|
242
|
+
|
|
243
|
+
await command(Ctx(msg.author.id, member.name if member else msg.author.display_name, msg.guild_id, msg.channel_id, say), arg.strip())
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
@client.event
|
|
247
|
+
async def on_interaction(i: zynko.Interaction) -> None:
|
|
248
|
+
# The buttons on the "Tocando agora" card: act, then refresh that card.
|
|
249
|
+
if i.type == "button":
|
|
250
|
+
p = client.voice.player if client.voice else None
|
|
251
|
+
if p and i.custom_id == "pausar":
|
|
252
|
+
p.resume() if p.paused else p.pause()
|
|
253
|
+
elif p and i.custom_id == "pular":
|
|
254
|
+
p.skip()
|
|
255
|
+
elif p and i.custom_id == "parar":
|
|
256
|
+
p.stop()
|
|
257
|
+
if i.custom_id != "pausar":
|
|
258
|
+
await asyncio.sleep(0.3) # a skip only shows the next song once it starts
|
|
259
|
+
await i.update(**now_playing())
|
|
260
|
+
return
|
|
261
|
+
|
|
262
|
+
async def say(answer: str | dict[str, Any], quiet: bool = False) -> Any:
|
|
263
|
+
if isinstance(answer, str):
|
|
264
|
+
return await i.reply(answer, ephemeral=quiet)
|
|
265
|
+
return await i.reply(**answer, ephemeral=quiet)
|
|
266
|
+
|
|
267
|
+
command = COMMANDS.get(i.command or "")
|
|
268
|
+
if not command:
|
|
269
|
+
await i.reply("Não conheço esse comando.", ephemeral=True)
|
|
270
|
+
return
|
|
271
|
+
who = i.member.name if i.member else i.user.display_name
|
|
272
|
+
value = i.option("link", i.option("porcentagem"))
|
|
273
|
+
arg = "" if value is None else str(value)
|
|
274
|
+
await command(Ctx(i.user.id, who, i.guild_id, i.channel_id, say), arg)
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
@client.event
|
|
278
|
+
async def on_ready(user: zynko.User) -> None:
|
|
279
|
+
await client.set_commands(SLASH)
|
|
280
|
+
print(f"{user.display_name} conectado em {len(client.guilds)} servidores, com {len(SLASH)} comandos")
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
@client.event
|
|
284
|
+
async def on_reconnecting(info: dict[str, Any]) -> None:
|
|
285
|
+
print(f"gateway caiu, reconectando em {info['wait']:.0f}s")
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
@client.event
|
|
289
|
+
async def on_error(error: Exception) -> None:
|
|
290
|
+
print("erro:", repr(error))
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
client.run(os.environ["ZYNKO_TOKEN"])
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""The smallest bot: answers !ping with pong.
|
|
2
|
+
|
|
3
|
+
ZYNKO_TOKEN=bot_... python ping.py
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import os
|
|
7
|
+
|
|
8
|
+
import zynko
|
|
9
|
+
|
|
10
|
+
client = zynko.Client(
|
|
11
|
+
intents=zynko.Intents.GUILDS | zynko.Intents.MESSAGES,
|
|
12
|
+
api=os.environ.get("ZYNKO_API"), # another server; https://zynko.org/api by default
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@client.event
|
|
17
|
+
async def on_ready(user: zynko.User) -> None:
|
|
18
|
+
print(f"{user.display_name} conectado em {len(client.guilds)} servidores")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@client.event
|
|
22
|
+
async def on_message_create(msg: zynko.Message) -> None:
|
|
23
|
+
if msg.mine:
|
|
24
|
+
return
|
|
25
|
+
if msg.content == "!ping":
|
|
26
|
+
await msg.reply("pong")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
client.run(os.environ["ZYNKO_TOKEN"])
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling==1.29.0"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "zynko"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Biblioteca oficial para bots do Zynko: API, gateway e voz."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
requires-python = ">=3.10"
|
|
12
|
+
keywords = ["zynko", "bot", "chat", "voice", "asyncio"]
|
|
13
|
+
classifiers = [
|
|
14
|
+
"Development Status :: 4 - Beta",
|
|
15
|
+
"Framework :: AsyncIO",
|
|
16
|
+
"Intended Audience :: Developers",
|
|
17
|
+
"Programming Language :: Python :: 3",
|
|
18
|
+
"Programming Language :: Python :: 3.10",
|
|
19
|
+
"Programming Language :: Python :: 3.11",
|
|
20
|
+
"Programming Language :: Python :: 3.12",
|
|
21
|
+
"Programming Language :: Python :: 3.13",
|
|
22
|
+
"Programming Language :: Python :: 3.14",
|
|
23
|
+
"Typing :: Typed",
|
|
24
|
+
]
|
|
25
|
+
dependencies = ["aiohttp>=3.11,<4"]
|
|
26
|
+
|
|
27
|
+
[project.optional-dependencies]
|
|
28
|
+
voice = ["livekit==1.2.1"]
|
|
29
|
+
dev = ["pytest>=8.3", "pytest-asyncio>=0.24", "mypy>=1.13"]
|
|
30
|
+
|
|
31
|
+
[project.urls]
|
|
32
|
+
Documentation = "https://docs.zynko.org/bots/python"
|
|
33
|
+
Homepage = "https://zynko.org"
|
|
34
|
+
|
|
35
|
+
[tool.hatch.build.targets.wheel]
|
|
36
|
+
packages = ["src/zynko"]
|
|
37
|
+
|
|
38
|
+
[tool.hatch.build.targets.sdist]
|
|
39
|
+
include = ["src/zynko", "README.md", "tests", "examples"]
|
|
40
|
+
|
|
41
|
+
[tool.pytest.ini_options]
|
|
42
|
+
testpaths = ["tests"]
|
|
43
|
+
asyncio_mode = "auto"
|
|
44
|
+
asyncio_default_fixture_loop_scope = "function"
|
|
45
|
+
|
|
46
|
+
[tool.mypy]
|
|
47
|
+
python_version = "3.10"
|
|
48
|
+
strict = true
|
|
49
|
+
ignore_missing_imports = true
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""zynko: the official library for Zynko bots (docs.zynko.org/bots).
|
|
2
|
+
|
|
3
|
+
API and gateway on asyncio and aiohttp; voice through the ``livekit`` package, installed with
|
|
4
|
+
``pip install "zynko[voice]"`` and loaded only when a bot joins a voice room.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from .bits import ALL_PERMISSIONS, Intents, Permissions, id_time
|
|
8
|
+
from .client import Client
|
|
9
|
+
from .gateway import GatewayClosed
|
|
10
|
+
from .interaction import Interaction
|
|
11
|
+
from .message import Message
|
|
12
|
+
from .models import (
|
|
13
|
+
Attachment,
|
|
14
|
+
Button,
|
|
15
|
+
ButtonStyle,
|
|
16
|
+
Card,
|
|
17
|
+
CardField,
|
|
18
|
+
Channel,
|
|
19
|
+
ChannelType,
|
|
20
|
+
Command,
|
|
21
|
+
CommandOption,
|
|
22
|
+
Components,
|
|
23
|
+
Emoji,
|
|
24
|
+
Guild,
|
|
25
|
+
Member,
|
|
26
|
+
Overwrite,
|
|
27
|
+
Role,
|
|
28
|
+
User,
|
|
29
|
+
VoiceState,
|
|
30
|
+
)
|
|
31
|
+
from .permissions import compute_permissions
|
|
32
|
+
from .player import PCM, AudioInput, AudioPlayer, Stream, Track, tone
|
|
33
|
+
from .rest import File, Rest, ZynkoError
|
|
34
|
+
|
|
35
|
+
__version__ = "0.1.0"
|
|
36
|
+
|
|
37
|
+
__all__ = [
|
|
38
|
+
"ALL_PERMISSIONS",
|
|
39
|
+
"Attachment",
|
|
40
|
+
"AudioInput",
|
|
41
|
+
"AudioPlayer",
|
|
42
|
+
"Button",
|
|
43
|
+
"ButtonStyle",
|
|
44
|
+
"Card",
|
|
45
|
+
"CardField",
|
|
46
|
+
"Channel",
|
|
47
|
+
"ChannelType",
|
|
48
|
+
"Client",
|
|
49
|
+
"Command",
|
|
50
|
+
"CommandOption",
|
|
51
|
+
"Components",
|
|
52
|
+
"Emoji",
|
|
53
|
+
"File",
|
|
54
|
+
"GatewayClosed",
|
|
55
|
+
"Guild",
|
|
56
|
+
"Intents",
|
|
57
|
+
"Interaction",
|
|
58
|
+
"Member",
|
|
59
|
+
"Message",
|
|
60
|
+
"Overwrite",
|
|
61
|
+
"PCM",
|
|
62
|
+
"Permissions",
|
|
63
|
+
"Rest",
|
|
64
|
+
"Role",
|
|
65
|
+
"Stream",
|
|
66
|
+
"Track",
|
|
67
|
+
"User",
|
|
68
|
+
"VoiceState",
|
|
69
|
+
"ZynkoError",
|
|
70
|
+
"compute_permissions",
|
|
71
|
+
"id_time",
|
|
72
|
+
"tone",
|
|
73
|
+
"__version__",
|
|
74
|
+
]
|