zynko 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- zynko/__init__.py +74 -0
- zynko/_events.py +123 -0
- zynko/bits.py +62 -0
- zynko/client.py +471 -0
- zynko/gateway.py +171 -0
- zynko/interaction.py +106 -0
- zynko/message.py +121 -0
- zynko/models.py +387 -0
- zynko/permissions.py +52 -0
- zynko/player.py +413 -0
- zynko/py.typed +0 -0
- zynko/rest.py +309 -0
- zynko/voice.py +298 -0
- zynko-0.1.0.dist-info/METADATA +67 -0
- zynko-0.1.0.dist-info/RECORD +16 -0
- zynko-0.1.0.dist-info/WHEEL +4 -0
zynko/interaction.py
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"""Someone used one of the bot's "/" commands, or clicked a button on one of its messages."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING, Any, Literal
|
|
6
|
+
|
|
7
|
+
from .message import Message
|
|
8
|
+
from .models import Card, Channel, Components, Member, User
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from .client import Client
|
|
12
|
+
|
|
13
|
+
__all__ = ["Interaction"]
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class Interaction:
|
|
17
|
+
"""A command or a click. Answer within 15 minutes.
|
|
18
|
+
|
|
19
|
+
:meth:`reply` posts a message (or one only they see), :meth:`update` changes the message
|
|
20
|
+
whose button was clicked::
|
|
21
|
+
|
|
22
|
+
@client.on("interaction")
|
|
23
|
+
async def on_interaction(i: zynko.Interaction):
|
|
24
|
+
if i.command == "dado":
|
|
25
|
+
await i.reply(f"🎲 {random.randint(1, i.option('lados', 6))}")
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
def __init__(self, client: Client, data: dict[str, Any]) -> None:
|
|
29
|
+
self.client = client
|
|
30
|
+
self.raw = data
|
|
31
|
+
self.id: str = str(data["id"])
|
|
32
|
+
self.token: str = data["token"]
|
|
33
|
+
self.type: Literal["command", "button"] = data.get("type", "command")
|
|
34
|
+
self.channel_id: str = str(data["channel_id"])
|
|
35
|
+
self.guild_id: str | None = data.get("guild_id")
|
|
36
|
+
#: Who used it.
|
|
37
|
+
self.user = User.from_dict(data["user"])
|
|
38
|
+
self.member = Member.from_dict(data["member"]) if data.get("member") else None
|
|
39
|
+
command = data.get("command") or {}
|
|
40
|
+
#: The command's name, for commands.
|
|
41
|
+
self.command: str | None = command.get("name")
|
|
42
|
+
#: What was typed for each option, by name.
|
|
43
|
+
self.options: dict[str, str | int | float | bool] = {o["name"]: o.get("value") for o in command.get("options") or []}
|
|
44
|
+
#: The clicked button's ``custom_id``, for buttons.
|
|
45
|
+
self.custom_id: str | None = data.get("custom_id")
|
|
46
|
+
#: The message whose button was clicked.
|
|
47
|
+
self.message = Message(client, data["message"]) if data.get("message") else None
|
|
48
|
+
#: Whether it was answered already.
|
|
49
|
+
self.replied = False
|
|
50
|
+
|
|
51
|
+
def __repr__(self) -> str:
|
|
52
|
+
what = f"command={self.command!r}" if self.type == "command" else f"custom_id={self.custom_id!r}"
|
|
53
|
+
return f"<Interaction id={self.id} type={self.type} {what} user={self.user.username!r}>"
|
|
54
|
+
|
|
55
|
+
def option(self, name: str, default: Any = None) -> Any:
|
|
56
|
+
"""One option's value; ``default`` when it was left out. User, channel and role options are ids."""
|
|
57
|
+
return self.options.get(name, default)
|
|
58
|
+
|
|
59
|
+
@property
|
|
60
|
+
def channel(self) -> Channel | None:
|
|
61
|
+
return self.client.channels.get(self.channel_id)
|
|
62
|
+
|
|
63
|
+
async def reply(
|
|
64
|
+
self,
|
|
65
|
+
content: str | None = None,
|
|
66
|
+
*,
|
|
67
|
+
cards: list[Card] | None = None,
|
|
68
|
+
components: Components | None = None,
|
|
69
|
+
ephemeral: bool = False,
|
|
70
|
+
) -> Message:
|
|
71
|
+
"""Answers with a message in the channel, or only for that person with ``ephemeral=True``.
|
|
72
|
+
|
|
73
|
+
May be called again for follow-ups.
|
|
74
|
+
"""
|
|
75
|
+
body: dict[str, Any] = {"type": "message"}
|
|
76
|
+
if content is not None:
|
|
77
|
+
body["content"] = content
|
|
78
|
+
if cards is not None:
|
|
79
|
+
body["cards"] = cards
|
|
80
|
+
if components is not None:
|
|
81
|
+
body["components"] = components
|
|
82
|
+
if ephemeral:
|
|
83
|
+
body["ephemeral"] = True
|
|
84
|
+
msg = await self.client.rest.interaction_callback(self.id, self.token, body)
|
|
85
|
+
self.replied = True
|
|
86
|
+
return Message(self.client, msg)
|
|
87
|
+
|
|
88
|
+
async def update(
|
|
89
|
+
self, content: str | None = None, *, cards: list[Card] | None = None, components: Components | None = None
|
|
90
|
+
) -> Message:
|
|
91
|
+
"""Changes the message whose button was clicked: whatever is given (text, cards, buttons).
|
|
92
|
+
|
|
93
|
+
``components=[]`` removes the buttons.
|
|
94
|
+
"""
|
|
95
|
+
if self.type != "button":
|
|
96
|
+
raise RuntimeError("update() é só para cliques em botões")
|
|
97
|
+
body: dict[str, Any] = {"type": "update"}
|
|
98
|
+
if content is not None:
|
|
99
|
+
body["content"] = content
|
|
100
|
+
if cards is not None:
|
|
101
|
+
body["cards"] = cards
|
|
102
|
+
if components is not None:
|
|
103
|
+
body["components"] = components
|
|
104
|
+
msg = await self.client.rest.interaction_callback(self.id, self.token, body)
|
|
105
|
+
self.replied = True
|
|
106
|
+
return Message(self.client, msg)
|
zynko/message.py
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
"""A message, with shortcuts to answer it, react, edit and delete."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING, Any
|
|
6
|
+
|
|
7
|
+
from .models import Attachment, Card, Channel, Components, Member, User
|
|
8
|
+
from .rest import FileInput
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from .client import Client
|
|
12
|
+
|
|
13
|
+
__all__ = ["Message"]
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class Message:
|
|
17
|
+
"""A message as the API sends it. ``raw`` keeps the JSON."""
|
|
18
|
+
|
|
19
|
+
__slots__ = (
|
|
20
|
+
"client",
|
|
21
|
+
"raw",
|
|
22
|
+
"id",
|
|
23
|
+
"channel_id",
|
|
24
|
+
"author",
|
|
25
|
+
"content",
|
|
26
|
+
"timestamp",
|
|
27
|
+
"edited_timestamp",
|
|
28
|
+
"reply_to",
|
|
29
|
+
"attachments",
|
|
30
|
+
"reactions",
|
|
31
|
+
"embeds",
|
|
32
|
+
"cards",
|
|
33
|
+
"components",
|
|
34
|
+
"interaction",
|
|
35
|
+
"ephemeral",
|
|
36
|
+
"nonce",
|
|
37
|
+
"partial",
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
def __init__(self, client: Client, data: dict[str, Any]) -> None:
|
|
41
|
+
self.client = client
|
|
42
|
+
self.raw = data
|
|
43
|
+
self.id: str = str(data["id"])
|
|
44
|
+
self.channel_id: str = str(data["channel_id"])
|
|
45
|
+
#: A MESSAGE_UPDATE brings only what changed (``id``, ``channel_id``, ``content``,
|
|
46
|
+
#: ``edited_timestamp``, ``embeds``): then ``partial`` is True and ``author`` is empty.
|
|
47
|
+
self.partial = "author" not in data
|
|
48
|
+
self.author = User.from_dict(data["author"]) if not self.partial else User("", "", "", None, False)
|
|
49
|
+
self.content: str = data.get("content") or ""
|
|
50
|
+
self.timestamp: int = int(data.get("timestamp") or 0)
|
|
51
|
+
self.edited_timestamp: int | None = data.get("edited_timestamp")
|
|
52
|
+
#: The message this one answers: ``{id, author, content}``.
|
|
53
|
+
self.reply_to: dict[str, Any] | None = data.get("reply_to")
|
|
54
|
+
self.attachments = [Attachment.from_dict(a) for a in data.get("attachments") or []]
|
|
55
|
+
#: ``{emoji, count, me}`` each.
|
|
56
|
+
self.reactions: list[dict[str, Any]] = data.get("reactions") or []
|
|
57
|
+
#: Link previews.
|
|
58
|
+
self.embeds: list[dict[str, Any]] = data.get("embeds") or []
|
|
59
|
+
self.cards: list[Card] = data.get("cards") or []
|
|
60
|
+
self.components: Components = data.get("components") or []
|
|
61
|
+
#: The command or click this (bot) message answers: ``{id, name?, user}``.
|
|
62
|
+
self.interaction: dict[str, Any] | None = data.get("interaction")
|
|
63
|
+
#: Only its reader sees it, and it is not kept.
|
|
64
|
+
self.ephemeral = bool(data.get("ephemeral", False))
|
|
65
|
+
self.nonce: str | None = data.get("nonce")
|
|
66
|
+
|
|
67
|
+
def __repr__(self) -> str:
|
|
68
|
+
return f"<Message id={self.id} channel_id={self.channel_id} author={self.author.username!r} content={self.content[:40]!r}>"
|
|
69
|
+
|
|
70
|
+
@property
|
|
71
|
+
def channel(self) -> Channel | None:
|
|
72
|
+
"""The channel, when the client has seen it."""
|
|
73
|
+
return self.client.channels.get(self.channel_id)
|
|
74
|
+
|
|
75
|
+
@property
|
|
76
|
+
def guild_id(self) -> str | None:
|
|
77
|
+
"""The guild (servidor) the message is in; None in a DM."""
|
|
78
|
+
c = self.channel
|
|
79
|
+
return c.guild_id if c else None
|
|
80
|
+
|
|
81
|
+
@property
|
|
82
|
+
def member(self) -> Member | None:
|
|
83
|
+
"""The author as a member of the guild (nickname, roles); None in DMs."""
|
|
84
|
+
g = self.guild_id
|
|
85
|
+
guild = self.client.guilds.get(g) if g else None
|
|
86
|
+
return guild.members.get(self.author.id) if guild else None
|
|
87
|
+
|
|
88
|
+
@property
|
|
89
|
+
def mine(self) -> bool:
|
|
90
|
+
"""Whether the author is the bot itself."""
|
|
91
|
+
return self.client.user is not None and self.author.id == self.client.user.id
|
|
92
|
+
|
|
93
|
+
async def reply(
|
|
94
|
+
self,
|
|
95
|
+
content: str | None = None,
|
|
96
|
+
*,
|
|
97
|
+
files: list[FileInput] | None = None,
|
|
98
|
+
cards: list[Card] | None = None,
|
|
99
|
+
components: Components | None = None,
|
|
100
|
+
nonce: str | None = None,
|
|
101
|
+
) -> Message:
|
|
102
|
+
"""Answers in the same channel, as a reply to this message."""
|
|
103
|
+
return await self.client.send(
|
|
104
|
+
self.channel_id, content, reply_to=self.id, files=files, cards=cards, components=components, nonce=nonce
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
async def react(self, emoji: str) -> None:
|
|
108
|
+
await self.client.rest.react(self.channel_id, self.id, emoji)
|
|
109
|
+
|
|
110
|
+
async def unreact(self, emoji: str) -> None:
|
|
111
|
+
await self.client.rest.unreact(self.channel_id, self.id, emoji)
|
|
112
|
+
|
|
113
|
+
async def edit(
|
|
114
|
+
self, content: str | None = None, *, cards: list[Card] | None = None, components: Components | None = None
|
|
115
|
+
) -> Message:
|
|
116
|
+
"""Only the author edits: a bot edits its own messages (text, cards, buttons)."""
|
|
117
|
+
data = await self.client.rest.edit_message(self.channel_id, self.id, content, cards=cards, components=components)
|
|
118
|
+
return Message(self.client, data)
|
|
119
|
+
|
|
120
|
+
async def delete(self) -> None:
|
|
121
|
+
await self.client.rest.delete_message(self.channel_id, self.id)
|
zynko/models.py
ADDED
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
"""The Zynko API's objects, as the server sends them (docs.zynko.org/api/objetos).
|
|
2
|
+
|
|
3
|
+
Ids are strings; times are milliseconds since 1970. Each model keeps the JSON it came from in
|
|
4
|
+
``raw``, so fields this library does not know yet are still reachable.
|
|
5
|
+
|
|
6
|
+
What a bot *sends* (cards, buttons, commands) is plain dicts, typed with ``TypedDict``.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from dataclasses import dataclass, field
|
|
12
|
+
from typing import Any, Literal, TypedDict
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"ChannelType",
|
|
16
|
+
"User",
|
|
17
|
+
"Overwrite",
|
|
18
|
+
"Channel",
|
|
19
|
+
"Member",
|
|
20
|
+
"Role",
|
|
21
|
+
"Emoji",
|
|
22
|
+
"Attachment",
|
|
23
|
+
"VoiceState",
|
|
24
|
+
"Guild",
|
|
25
|
+
"Card",
|
|
26
|
+
"CardAuthor",
|
|
27
|
+
"CardField",
|
|
28
|
+
"CardFooter",
|
|
29
|
+
"CardImage",
|
|
30
|
+
"Button",
|
|
31
|
+
"ButtonStyle",
|
|
32
|
+
"Components",
|
|
33
|
+
"Command",
|
|
34
|
+
"CommandOption",
|
|
35
|
+
"CommandChoice",
|
|
36
|
+
"OptionType",
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
JSON = dict[str, Any]
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class ChannelType:
|
|
43
|
+
TEXT = 0
|
|
44
|
+
DM = 1
|
|
45
|
+
VOICE = 2
|
|
46
|
+
CATEGORY = 4
|
|
47
|
+
POST = 11
|
|
48
|
+
FORUM = 15
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@dataclass(slots=True)
|
|
52
|
+
class User:
|
|
53
|
+
id: str
|
|
54
|
+
username: str
|
|
55
|
+
display_name: str
|
|
56
|
+
avatar: str | None
|
|
57
|
+
bot: bool
|
|
58
|
+
raw: JSON = field(default_factory=dict, repr=False, compare=False)
|
|
59
|
+
|
|
60
|
+
@classmethod
|
|
61
|
+
def from_dict(cls, d: JSON) -> User:
|
|
62
|
+
return cls(
|
|
63
|
+
id=str(d["id"]),
|
|
64
|
+
username=d.get("username", ""),
|
|
65
|
+
display_name=d.get("display_name") or d.get("username", ""),
|
|
66
|
+
avatar=d.get("avatar"),
|
|
67
|
+
bot=bool(d.get("bot", False)),
|
|
68
|
+
raw=d,
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@dataclass(slots=True)
|
|
73
|
+
class Overwrite:
|
|
74
|
+
"""A channel's permission overwrite: ``type`` 0 is a role, 1 a member."""
|
|
75
|
+
|
|
76
|
+
id: str
|
|
77
|
+
type: int
|
|
78
|
+
allow: int
|
|
79
|
+
deny: int
|
|
80
|
+
|
|
81
|
+
@classmethod
|
|
82
|
+
def from_dict(cls, d: JSON) -> Overwrite:
|
|
83
|
+
return cls(id=str(d["id"]), type=int(d.get("type", 0)), allow=int(d.get("allow", 0)), deny=int(d.get("deny", 0)))
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@dataclass(slots=True)
|
|
87
|
+
class Channel:
|
|
88
|
+
id: str
|
|
89
|
+
guild_id: str | None
|
|
90
|
+
type: int
|
|
91
|
+
name: str
|
|
92
|
+
topic: str | None = None
|
|
93
|
+
position: int = 0
|
|
94
|
+
parent_id: str | None = None
|
|
95
|
+
last_message_id: str | None = None
|
|
96
|
+
#: The voice server a room runs on, when someone picked one.
|
|
97
|
+
voice_server: str | None = None
|
|
98
|
+
#: In a DM: the other person.
|
|
99
|
+
recipients: list[User] = field(default_factory=list)
|
|
100
|
+
permission_overwrites: list[Overwrite] = field(default_factory=list)
|
|
101
|
+
raw: JSON = field(default_factory=dict, repr=False, compare=False)
|
|
102
|
+
|
|
103
|
+
@classmethod
|
|
104
|
+
def from_dict(cls, d: JSON) -> Channel:
|
|
105
|
+
return cls(
|
|
106
|
+
id=str(d["id"]),
|
|
107
|
+
guild_id=d.get("guild_id"),
|
|
108
|
+
type=int(d.get("type", 0)),
|
|
109
|
+
name=d.get("name") or "",
|
|
110
|
+
topic=d.get("topic"),
|
|
111
|
+
position=int(d.get("position") or 0),
|
|
112
|
+
parent_id=d.get("parent_id"),
|
|
113
|
+
last_message_id=d.get("last_message_id"),
|
|
114
|
+
voice_server=d.get("voice_server"),
|
|
115
|
+
recipients=[User.from_dict(u) for u in d.get("recipients") or []],
|
|
116
|
+
permission_overwrites=[Overwrite.from_dict(o) for o in d.get("permission_overwrites") or []],
|
|
117
|
+
raw=d,
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
@dataclass(slots=True)
|
|
122
|
+
class Member:
|
|
123
|
+
guild_id: str
|
|
124
|
+
user: User
|
|
125
|
+
nick: str | None
|
|
126
|
+
is_admin: bool
|
|
127
|
+
joined_at: int
|
|
128
|
+
#: Not including @everyone, which everybody has.
|
|
129
|
+
roles: list[str]
|
|
130
|
+
raw: JSON = field(default_factory=dict, repr=False, compare=False)
|
|
131
|
+
|
|
132
|
+
@classmethod
|
|
133
|
+
def from_dict(cls, d: JSON) -> Member:
|
|
134
|
+
return cls(
|
|
135
|
+
guild_id=str(d.get("guild_id", "")),
|
|
136
|
+
user=User.from_dict(d["user"]),
|
|
137
|
+
nick=d.get("nick"),
|
|
138
|
+
is_admin=bool(d.get("is_admin", False)),
|
|
139
|
+
joined_at=int(d.get("joined_at") or 0),
|
|
140
|
+
roles=[str(r) for r in d.get("roles") or []],
|
|
141
|
+
raw=d,
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
@property
|
|
145
|
+
def name(self) -> str:
|
|
146
|
+
"""The nickname, or the account's display name."""
|
|
147
|
+
return self.nick or self.user.display_name
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
@dataclass(slots=True)
|
|
151
|
+
class Role:
|
|
152
|
+
id: str
|
|
153
|
+
guild_id: str
|
|
154
|
+
name: str
|
|
155
|
+
color: int | None
|
|
156
|
+
position: int
|
|
157
|
+
permissions: int
|
|
158
|
+
hoist: bool
|
|
159
|
+
raw: JSON = field(default_factory=dict, repr=False, compare=False)
|
|
160
|
+
|
|
161
|
+
@classmethod
|
|
162
|
+
def from_dict(cls, d: JSON) -> Role:
|
|
163
|
+
return cls(
|
|
164
|
+
id=str(d["id"]),
|
|
165
|
+
guild_id=str(d.get("guild_id", "")),
|
|
166
|
+
name=d.get("name", ""),
|
|
167
|
+
color=d.get("color"),
|
|
168
|
+
position=int(d.get("position") or 0),
|
|
169
|
+
permissions=int(d.get("permissions") or 0),
|
|
170
|
+
hoist=bool(d.get("hoist", False)),
|
|
171
|
+
raw=d,
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
@dataclass(slots=True)
|
|
176
|
+
class Emoji:
|
|
177
|
+
id: str
|
|
178
|
+
guild_id: str
|
|
179
|
+
name: str
|
|
180
|
+
url: str
|
|
181
|
+
animated: bool
|
|
182
|
+
raw: JSON = field(default_factory=dict, repr=False, compare=False)
|
|
183
|
+
|
|
184
|
+
@classmethod
|
|
185
|
+
def from_dict(cls, d: JSON) -> Emoji:
|
|
186
|
+
return cls(
|
|
187
|
+
id=str(d["id"]),
|
|
188
|
+
guild_id=str(d.get("guild_id", "")),
|
|
189
|
+
name=d.get("name", ""),
|
|
190
|
+
url=d.get("url", ""),
|
|
191
|
+
animated=bool(d.get("animated", False)),
|
|
192
|
+
raw=d,
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
@dataclass(slots=True)
|
|
197
|
+
class Attachment:
|
|
198
|
+
id: str
|
|
199
|
+
filename: str
|
|
200
|
+
content_type: str
|
|
201
|
+
size: int
|
|
202
|
+
#: ``/api/attachments/{id}``: needs the token to download.
|
|
203
|
+
url: str
|
|
204
|
+
raw: JSON = field(default_factory=dict, repr=False, compare=False)
|
|
205
|
+
|
|
206
|
+
@classmethod
|
|
207
|
+
def from_dict(cls, d: JSON) -> Attachment:
|
|
208
|
+
return cls(
|
|
209
|
+
id=str(d["id"]),
|
|
210
|
+
filename=d.get("filename", ""),
|
|
211
|
+
content_type=d.get("content_type", ""),
|
|
212
|
+
size=int(d.get("size") or 0),
|
|
213
|
+
url=d.get("url", ""),
|
|
214
|
+
raw=d,
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
@dataclass(slots=True)
|
|
219
|
+
class VoiceState:
|
|
220
|
+
user_id: str
|
|
221
|
+
guild_id: str | None
|
|
222
|
+
#: None when the account left voice.
|
|
223
|
+
channel_id: str | None
|
|
224
|
+
self_mute: bool = False
|
|
225
|
+
self_deaf: bool = False
|
|
226
|
+
self_stream: bool = False
|
|
227
|
+
self_video: bool = False
|
|
228
|
+
session_id: str = ""
|
|
229
|
+
raw: JSON = field(default_factory=dict, repr=False, compare=False)
|
|
230
|
+
|
|
231
|
+
@classmethod
|
|
232
|
+
def from_dict(cls, d: JSON) -> VoiceState:
|
|
233
|
+
return cls(
|
|
234
|
+
user_id=str(d["user_id"]),
|
|
235
|
+
guild_id=d.get("guild_id"),
|
|
236
|
+
channel_id=d.get("channel_id"),
|
|
237
|
+
self_mute=bool(d.get("self_mute", False)),
|
|
238
|
+
self_deaf=bool(d.get("self_deaf", False)),
|
|
239
|
+
self_stream=bool(d.get("self_stream", False)),
|
|
240
|
+
self_video=bool(d.get("self_video", False)),
|
|
241
|
+
session_id=d.get("session_id") or "",
|
|
242
|
+
raw=d,
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
@dataclass(slots=True)
|
|
247
|
+
class Guild:
|
|
248
|
+
"""A guild (servidor) as the client keeps it, updated by the gateway's events."""
|
|
249
|
+
|
|
250
|
+
id: str
|
|
251
|
+
name: str
|
|
252
|
+
icon: str | None
|
|
253
|
+
owner_id: str
|
|
254
|
+
channels: dict[str, Channel] = field(default_factory=dict)
|
|
255
|
+
#: By user id.
|
|
256
|
+
members: dict[str, Member] = field(default_factory=dict)
|
|
257
|
+
roles: dict[str, Role] = field(default_factory=dict)
|
|
258
|
+
emojis: list[Emoji] = field(default_factory=list)
|
|
259
|
+
#: Ids of members online now (needs the PRESENCES intent to stay current).
|
|
260
|
+
online: set[str] = field(default_factory=set)
|
|
261
|
+
raw: JSON = field(default_factory=dict, repr=False, compare=False)
|
|
262
|
+
|
|
263
|
+
@classmethod
|
|
264
|
+
def from_dict(cls, d: JSON) -> Guild:
|
|
265
|
+
channels = [Channel.from_dict(c) for c in d.get("channels") or []]
|
|
266
|
+
members = [Member.from_dict(m) for m in d.get("members") or []]
|
|
267
|
+
roles = [Role.from_dict(r) for r in d.get("roles") or []]
|
|
268
|
+
return cls(
|
|
269
|
+
id=str(d["id"]),
|
|
270
|
+
name=d.get("name", ""),
|
|
271
|
+
icon=d.get("icon"),
|
|
272
|
+
owner_id=str(d.get("owner_id", "")),
|
|
273
|
+
channels={c.id: c for c in channels},
|
|
274
|
+
members={m.user.id: m for m in members},
|
|
275
|
+
roles={r.id: r for r in roles},
|
|
276
|
+
emojis=[Emoji.from_dict(e) for e in d.get("emojis") or []],
|
|
277
|
+
online={str(p) for p in d.get("presences") or []},
|
|
278
|
+
raw=d,
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
# ---------- what a bot sends ----------
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
class _CardAuthor(TypedDict):
|
|
286
|
+
name: str
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
class CardAuthor(_CardAuthor, total=False):
|
|
290
|
+
url: str
|
|
291
|
+
icon_url: str
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
class _CardField(TypedDict):
|
|
295
|
+
name: str
|
|
296
|
+
value: str
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
class CardField(_CardField, total=False):
|
|
300
|
+
inline: bool
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
class CardImage(TypedDict):
|
|
304
|
+
url: str
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
class _CardFooter(TypedDict):
|
|
308
|
+
text: str
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
class CardFooter(_CardFooter, total=False):
|
|
312
|
+
icon_url: str
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
class Card(TypedDict, total=False):
|
|
316
|
+
"""A bot's card: a title, text, fields and pictures with a colored edge."""
|
|
317
|
+
|
|
318
|
+
title: str
|
|
319
|
+
#: Markdown, like a message.
|
|
320
|
+
description: str
|
|
321
|
+
#: Where the title points.
|
|
322
|
+
url: str
|
|
323
|
+
#: The edge's color, 0xRRGGBB.
|
|
324
|
+
color: int
|
|
325
|
+
author: CardAuthor
|
|
326
|
+
fields: list[CardField]
|
|
327
|
+
#: Wide, under the text.
|
|
328
|
+
image: CardImage
|
|
329
|
+
#: Small, beside the text.
|
|
330
|
+
thumbnail: CardImage
|
|
331
|
+
footer: CardFooter
|
|
332
|
+
#: Shown in the footer, in ms.
|
|
333
|
+
timestamp: int
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
ButtonStyle = Literal["primary", "secondary", "success", "danger", "link"]
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
class _Button(TypedDict):
|
|
340
|
+
style: ButtonStyle
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
class Button(_Button, total=False):
|
|
344
|
+
label: str
|
|
345
|
+
emoji: str
|
|
346
|
+
#: What the bot gets back when it is clicked (all but links).
|
|
347
|
+
custom_id: str
|
|
348
|
+
#: Link buttons only.
|
|
349
|
+
url: str
|
|
350
|
+
disabled: bool
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
#: Rows of buttons, top to bottom: up to 5 rows of 5.
|
|
354
|
+
Components = list[list[Button]]
|
|
355
|
+
|
|
356
|
+
OptionType = Literal["string", "integer", "number", "boolean", "user", "channel", "role"]
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
class CommandChoice(TypedDict):
|
|
360
|
+
name: str
|
|
361
|
+
value: str | int | float
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
class _CommandOption(TypedDict):
|
|
365
|
+
name: str
|
|
366
|
+
description: str
|
|
367
|
+
type: OptionType
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
class CommandOption(_CommandOption, total=False):
|
|
371
|
+
required: bool
|
|
372
|
+
#: The only values allowed.
|
|
373
|
+
choices: list[CommandChoice]
|
|
374
|
+
min_value: float
|
|
375
|
+
max_value: float
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
class _Command(TypedDict):
|
|
379
|
+
#: 1 to 32 lowercase letters, digits, _ or -.
|
|
380
|
+
name: str
|
|
381
|
+
description: str
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
class Command(_Command, total=False):
|
|
385
|
+
"""A "/" command (docs.zynko.org/bots/comandos)."""
|
|
386
|
+
|
|
387
|
+
options: list[CommandOption]
|
zynko/permissions.py
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""What someone may do in a channel, computed the way the server does."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from .bits import ALL_PERMISSIONS, Permissions
|
|
6
|
+
from .models import Channel, Guild
|
|
7
|
+
|
|
8
|
+
__all__ = ["compute_permissions"]
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def compute_permissions(guild: Guild | None, channel: Channel | None, user_id: str) -> Permissions:
|
|
12
|
+
"""@everyone and the member's roles, then the channel's overwrites.
|
|
13
|
+
|
|
14
|
+
Owners and admins may do everything. Without VIEW_CHANNEL nothing is left; without
|
|
15
|
+
SEND_MESSAGES no ATTACH_FILES; without CONNECT no SPEAK or STREAM.
|
|
16
|
+
"""
|
|
17
|
+
P = Permissions
|
|
18
|
+
if channel is None or guild is None:
|
|
19
|
+
return P(0)
|
|
20
|
+
member = guild.members.get(user_id)
|
|
21
|
+
if member is None:
|
|
22
|
+
return P(0)
|
|
23
|
+
if guild.owner_id == user_id or member.is_admin:
|
|
24
|
+
return P(ALL_PERMISSIONS)
|
|
25
|
+
everyone_role = guild.roles.get(guild.id)
|
|
26
|
+
base = everyone_role.permissions if everyone_role else 0
|
|
27
|
+
for r in member.roles:
|
|
28
|
+
role = guild.roles.get(r)
|
|
29
|
+
base |= role.permissions if role else 0
|
|
30
|
+
if base & P.ADMINISTRATOR:
|
|
31
|
+
return P(ALL_PERMISSIONS)
|
|
32
|
+
overwrites = channel.permission_overwrites
|
|
33
|
+
p = base
|
|
34
|
+
everyone = next((o for o in overwrites if o.type == 0 and o.id == guild.id), None)
|
|
35
|
+
if everyone is not None:
|
|
36
|
+
p = (p & ~everyone.deny) | everyone.allow
|
|
37
|
+
allow = deny = 0
|
|
38
|
+
for o in overwrites:
|
|
39
|
+
if o.type == 0 and o.id != guild.id and o.id in member.roles:
|
|
40
|
+
allow |= o.allow
|
|
41
|
+
deny |= o.deny
|
|
42
|
+
p = (p & ~deny) | allow
|
|
43
|
+
own = next((o for o in overwrites if o.type == 1 and o.id == user_id), None)
|
|
44
|
+
if own is not None:
|
|
45
|
+
p = (p & ~own.deny) | own.allow
|
|
46
|
+
if not p & P.VIEW_CHANNEL:
|
|
47
|
+
return P(0)
|
|
48
|
+
if not p & P.SEND_MESSAGES:
|
|
49
|
+
p &= ~P.ATTACH_FILES
|
|
50
|
+
if not p & P.CONNECT:
|
|
51
|
+
p &= ~(P.SPEAK | P.STREAM)
|
|
52
|
+
return P(p)
|