loopbot-discord-sdk 1.0.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.
@@ -0,0 +1,17 @@
1
+ """
2
+ Context classes for handling interactions
3
+ """
4
+
5
+ from .base import BaseContext
6
+ from .command import CommandContext
7
+ from .button import ButtonContext
8
+ from .modal import ModalContext
9
+ from .select import SelectContext
10
+
11
+ __all__ = [
12
+ "BaseContext",
13
+ "CommandContext",
14
+ "ButtonContext",
15
+ "ModalContext",
16
+ "SelectContext",
17
+ ]
@@ -0,0 +1,263 @@
1
+ """
2
+ Base Context for all interaction types
3
+ """
4
+
5
+ from typing import Any, Dict, List, Optional, Union
6
+
7
+ from ..types import Interaction, InteractionResponseType, DiscordUser, DiscordMember
8
+ from ..builders.embed import EmbedBuilder
9
+ from ..builders.action_row import ActionRowBuilder
10
+ from ..builders.modal import ModalBuilder
11
+
12
+
13
+ class BaseContext:
14
+ """Base context for handling Discord interactions"""
15
+
16
+ def __init__(
17
+ self,
18
+ interaction: Dict[str, Any],
19
+ client: Any,
20
+ application_id: str,
21
+ ):
22
+ self._interaction = interaction
23
+ self._client = client
24
+ self._application_id = application_id
25
+ self._response: Optional[Dict[str, Any]] = None
26
+ self._deferred = False
27
+
28
+ @property
29
+ def interaction_id(self) -> str:
30
+ return self._interaction.get("id", "")
31
+
32
+ @property
33
+ def token(self) -> str:
34
+ return self._interaction.get("token", "")
35
+
36
+ @property
37
+ def guild_id(self) -> Optional[str]:
38
+ return self._interaction.get("guild_id")
39
+
40
+ @property
41
+ def channel_id(self) -> Optional[str]:
42
+ return self._interaction.get("channel_id")
43
+
44
+ @property
45
+ def user(self) -> Optional[Dict[str, Any]]:
46
+ """Get the user who triggered the interaction"""
47
+ member = self._interaction.get("member")
48
+ if member:
49
+ return member.get("user")
50
+ return self._interaction.get("user")
51
+
52
+ @property
53
+ def user_id(self) -> Optional[str]:
54
+ user = self.user
55
+ return user.get("id") if user else None
56
+
57
+ @property
58
+ def username(self) -> Optional[str]:
59
+ user = self.user
60
+ return user.get("username") if user else None
61
+
62
+ @property
63
+ def member(self) -> Optional[Dict[str, Any]]:
64
+ return self._interaction.get("member")
65
+
66
+ @property
67
+ def locale(self) -> Optional[str]:
68
+ return self._interaction.get("locale")
69
+
70
+ @property
71
+ def response(self) -> Optional[Dict[str, Any]]:
72
+ return self._response
73
+
74
+ @property
75
+ def is_deferred(self) -> bool:
76
+ return self._deferred
77
+
78
+ def reply(
79
+ self,
80
+ content: Optional[str] = None,
81
+ embeds: Optional[List[Union[Dict[str, Any], EmbedBuilder]]] = None,
82
+ components: Optional[List[Union[Dict[str, Any], ActionRowBuilder]]] = None,
83
+ ephemeral: bool = False,
84
+ ) -> None:
85
+ """Reply to the interaction"""
86
+ data: Dict[str, Any] = {}
87
+
88
+ if content:
89
+ data["content"] = content
90
+
91
+ if embeds:
92
+ data["embeds"] = [
93
+ e.to_dict() if isinstance(e, EmbedBuilder) else e
94
+ for e in embeds
95
+ ]
96
+
97
+ if components:
98
+ data["components"] = [
99
+ c.to_dict() if isinstance(c, ActionRowBuilder) else c
100
+ for c in components
101
+ ]
102
+
103
+ if ephemeral:
104
+ data["flags"] = 64
105
+
106
+ self._response = {
107
+ "type": InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
108
+ "data": data,
109
+ }
110
+
111
+ def reply_text(self, content: str, ephemeral: bool = False) -> None:
112
+ """Reply with simple text"""
113
+ self.reply(content=content, ephemeral=ephemeral)
114
+
115
+ def reply_with_components(
116
+ self,
117
+ components: List[Any],
118
+ ephemeral: bool = False,
119
+ ) -> None:
120
+ """Reply with Components V2 (Container, MediaGallery, etc.)"""
121
+ data: Dict[str, Any] = {
122
+ "components": [
123
+ c.to_dict() if hasattr(c, "to_dict") else c
124
+ for c in components
125
+ ],
126
+ "flags": 32768, # IS_COMPONENTS_V2
127
+ }
128
+
129
+ if ephemeral:
130
+ data["flags"] |= 64
131
+
132
+ self._response = {
133
+ "type": InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
134
+ "data": data,
135
+ }
136
+
137
+ def update(
138
+ self,
139
+ content: Optional[str] = None,
140
+ embeds: Optional[List[Union[Dict[str, Any], EmbedBuilder]]] = None,
141
+ components: Optional[List[Union[Dict[str, Any], ActionRowBuilder]]] = None,
142
+ ) -> None:
143
+ """Update the original message"""
144
+ data: Dict[str, Any] = {}
145
+
146
+ if content is not None:
147
+ data["content"] = content
148
+
149
+ if embeds:
150
+ data["embeds"] = [
151
+ e.to_dict() if isinstance(e, EmbedBuilder) else e
152
+ for e in embeds
153
+ ]
154
+
155
+ if components:
156
+ data["components"] = [
157
+ c.to_dict() if isinstance(c, ActionRowBuilder) else c
158
+ for c in components
159
+ ]
160
+
161
+ self._response = {
162
+ "type": InteractionResponseType.UPDATE_MESSAGE,
163
+ "data": data,
164
+ }
165
+
166
+ def update_with_components(self, components: List[Any]) -> None:
167
+ """Update the original message with Components V2"""
168
+ self._response = {
169
+ "type": InteractionResponseType.UPDATE_MESSAGE,
170
+ "data": {
171
+ "components": [
172
+ c.to_dict() if hasattr(c, "to_dict") else c
173
+ for c in components
174
+ ],
175
+ "flags": 32768, # IS_COMPONENTS_V2
176
+ },
177
+ }
178
+
179
+ def defer(self, ephemeral: bool = False) -> None:
180
+ """Defer the response (show 'thinking...')"""
181
+ self._deferred = True
182
+ self._response = {
183
+ "type": InteractionResponseType.DEFERRED_CHANNEL_MESSAGE_WITH_SOURCE,
184
+ "data": {"flags": 64 if ephemeral else 0},
185
+ }
186
+
187
+ def defer_update(self) -> None:
188
+ """Defer the update (acknowledge without visible loading)"""
189
+ self._deferred = True
190
+ self._response = {
191
+ "type": InteractionResponseType.DEFERRED_UPDATE_MESSAGE,
192
+ }
193
+
194
+ def show_modal(self, modal: ModalBuilder) -> None:
195
+ """Show a modal to the user"""
196
+ self._response = {
197
+ "type": InteractionResponseType.MODAL,
198
+ "data": modal.to_dict(),
199
+ }
200
+
201
+ async def follow_up(
202
+ self,
203
+ content: Optional[str] = None,
204
+ embeds: Optional[List[Union[Dict[str, Any], EmbedBuilder]]] = None,
205
+ components: Optional[List[Union[Dict[str, Any], ActionRowBuilder]]] = None,
206
+ ephemeral: bool = False,
207
+ ) -> Dict[str, Any]:
208
+ """Send a follow-up message"""
209
+ data: Dict[str, Any] = {}
210
+
211
+ if content:
212
+ data["content"] = content
213
+
214
+ if embeds:
215
+ data["embeds"] = [
216
+ e.to_dict() if isinstance(e, EmbedBuilder) else e
217
+ for e in embeds
218
+ ]
219
+
220
+ if components:
221
+ data["components"] = [
222
+ c.to_dict() if isinstance(c, ActionRowBuilder) else c
223
+ for c in components
224
+ ]
225
+
226
+ if ephemeral:
227
+ data["flags"] = 64
228
+
229
+ return await self._client.follow_up(
230
+ self._application_id,
231
+ self.token,
232
+ data,
233
+ )
234
+
235
+ async def edit_reply(
236
+ self,
237
+ content: Optional[str] = None,
238
+ embeds: Optional[List[Union[Dict[str, Any], EmbedBuilder]]] = None,
239
+ components: Optional[List[Union[Dict[str, Any], ActionRowBuilder]]] = None,
240
+ ) -> None:
241
+ """Edit the original response"""
242
+ data: Dict[str, Any] = {}
243
+
244
+ if content is not None:
245
+ data["content"] = content
246
+
247
+ if embeds:
248
+ data["embeds"] = [
249
+ e.to_dict() if isinstance(e, EmbedBuilder) else e
250
+ for e in embeds
251
+ ]
252
+
253
+ if components:
254
+ data["components"] = [
255
+ c.to_dict() if isinstance(c, ActionRowBuilder) else c
256
+ for c in components
257
+ ]
258
+
259
+ await self._client.edit_original(
260
+ self._application_id,
261
+ self.token,
262
+ data,
263
+ )
@@ -0,0 +1,28 @@
1
+ """
2
+ Button Context for button interactions
3
+ """
4
+
5
+ from typing import Any, Dict, Optional
6
+
7
+ from .base import BaseContext
8
+
9
+
10
+ class ButtonContext(BaseContext):
11
+ """Context for handling button interactions"""
12
+
13
+ @property
14
+ def custom_id(self) -> str:
15
+ """Get the button custom ID"""
16
+ data = self._interaction.get("data", {})
17
+ return data.get("custom_id", "")
18
+
19
+ @property
20
+ def message(self) -> Optional[Dict[str, Any]]:
21
+ """Get the message the button was attached to"""
22
+ return self._interaction.get("message")
23
+
24
+ @property
25
+ def message_id(self) -> Optional[str]:
26
+ """Get the message ID"""
27
+ message = self.message
28
+ return message.get("id") if message else None
@@ -0,0 +1,93 @@
1
+ """
2
+ Command Context for slash commands
3
+ """
4
+
5
+ from typing import Any, Dict, List, Optional
6
+
7
+ from .base import BaseContext
8
+
9
+
10
+ class CommandContext(BaseContext):
11
+ """Context for handling slash command interactions"""
12
+
13
+ @property
14
+ def command_name(self) -> str:
15
+ """Get the command name"""
16
+ data = self._interaction.get("data", {})
17
+ return data.get("name", "")
18
+
19
+ @property
20
+ def options(self) -> Dict[str, Any]:
21
+ """Get command options as a dict"""
22
+ data = self._interaction.get("data", {})
23
+ options = data.get("options", [])
24
+ return self._parse_options(options)
25
+
26
+ def _parse_options(self, options: List[Dict[str, Any]]) -> Dict[str, Any]:
27
+ """Parse options list into dict"""
28
+ result: Dict[str, Any] = {}
29
+ for opt in options:
30
+ name = opt.get("name", "")
31
+ value = opt.get("value")
32
+
33
+ # Handle subcommands/groups
34
+ if opt.get("options"):
35
+ result[name] = self._parse_options(opt["options"])
36
+ else:
37
+ result[name] = value
38
+
39
+ return result
40
+
41
+ def get_option(
42
+ self,
43
+ name: str,
44
+ default: Any = None,
45
+ ) -> Any:
46
+ """Get a specific option value"""
47
+ return self.options.get(name, default)
48
+
49
+ def get_string(self, name: str, default: str = "") -> str:
50
+ """Get a string option"""
51
+ return str(self.get_option(name, default))
52
+
53
+ def get_int(self, name: str, default: int = 0) -> int:
54
+ """Get an integer option"""
55
+ value = self.get_option(name, default)
56
+ return int(value) if value is not None else default
57
+
58
+ def get_bool(self, name: str, default: bool = False) -> bool:
59
+ """Get a boolean option"""
60
+ return bool(self.get_option(name, default))
61
+
62
+ def get_user(self, name: str) -> Optional[Dict[str, Any]]:
63
+ """Get a user option from resolved data"""
64
+ user_id = self.get_option(name)
65
+ if not user_id:
66
+ return None
67
+
68
+ data = self._interaction.get("data", {})
69
+ resolved = data.get("resolved", {})
70
+ users = resolved.get("users", {})
71
+ return users.get(user_id)
72
+
73
+ def get_channel(self, name: str) -> Optional[Dict[str, Any]]:
74
+ """Get a channel option from resolved data"""
75
+ channel_id = self.get_option(name)
76
+ if not channel_id:
77
+ return None
78
+
79
+ data = self._interaction.get("data", {})
80
+ resolved = data.get("resolved", {})
81
+ channels = resolved.get("channels", {})
82
+ return channels.get(channel_id)
83
+
84
+ def get_role(self, name: str) -> Optional[Dict[str, Any]]:
85
+ """Get a role option from resolved data"""
86
+ role_id = self.get_option(name)
87
+ if not role_id:
88
+ return None
89
+
90
+ data = self._interaction.get("data", {})
91
+ resolved = data.get("resolved", {})
92
+ roles = resolved.get("roles", {})
93
+ return roles.get(role_id)
@@ -0,0 +1,46 @@
1
+ """
2
+ Modal Context for modal submit interactions
3
+ """
4
+
5
+ from typing import Any, Dict, List, Optional
6
+
7
+ from .base import BaseContext
8
+
9
+
10
+ class ModalContext(BaseContext):
11
+ """Context for handling modal submit interactions"""
12
+
13
+ def __init__(self, *args: Any, **kwargs: Any):
14
+ super().__init__(*args, **kwargs)
15
+ self._fields: Dict[str, str] = self._parse_fields()
16
+
17
+ def _parse_fields(self) -> Dict[str, str]:
18
+ """Parse modal fields from interaction data"""
19
+ fields: Dict[str, str] = {}
20
+ data = self._interaction.get("data", {})
21
+ components = data.get("components", [])
22
+
23
+ for row in components:
24
+ row_components = row.get("components", [])
25
+ for component in row_components:
26
+ custom_id = component.get("custom_id", "")
27
+ value = component.get("value", "")
28
+ if custom_id:
29
+ fields[custom_id] = value
30
+
31
+ return fields
32
+
33
+ @property
34
+ def custom_id(self) -> str:
35
+ """Get the modal custom ID"""
36
+ data = self._interaction.get("data", {})
37
+ return data.get("custom_id", "")
38
+
39
+ @property
40
+ def fields(self) -> Dict[str, str]:
41
+ """Get all modal fields as dict"""
42
+ return self._fields
43
+
44
+ def get_field(self, custom_id: str, default: str = "") -> str:
45
+ """Get a specific field value"""
46
+ return self._fields.get(custom_id, default)
@@ -0,0 +1,34 @@
1
+ """
2
+ Select Context for select menu interactions
3
+ """
4
+
5
+ from typing import Any, Dict, List, Optional
6
+
7
+ from .base import BaseContext
8
+
9
+
10
+ class SelectContext(BaseContext):
11
+ """Context for handling select menu interactions"""
12
+
13
+ @property
14
+ def custom_id(self) -> str:
15
+ """Get the select menu custom ID"""
16
+ data = self._interaction.get("data", {})
17
+ return data.get("custom_id", "")
18
+
19
+ @property
20
+ def values(self) -> List[str]:
21
+ """Get selected values"""
22
+ data = self._interaction.get("data", {})
23
+ return data.get("values", [])
24
+
25
+ @property
26
+ def message(self) -> Optional[Dict[str, Any]]:
27
+ """Get the message the select was attached to"""
28
+ return self._interaction.get("message")
29
+
30
+ @property
31
+ def message_id(self) -> Optional[str]:
32
+ """Get the message ID"""
33
+ message = self.message
34
+ return message.get("id") if message else None
loopbot/types.py ADDED
@@ -0,0 +1,76 @@
1
+ """
2
+ Types and enums for the Loop Discord SDK
3
+ """
4
+
5
+ from enum import IntEnum
6
+ from typing import Any, Dict, List, Optional, TypedDict
7
+
8
+
9
+ class InteractionType(IntEnum):
10
+ PING = 1
11
+ APPLICATION_COMMAND = 2
12
+ MESSAGE_COMPONENT = 3
13
+ APPLICATION_COMMAND_AUTOCOMPLETE = 4
14
+ MODAL_SUBMIT = 5
15
+
16
+
17
+ class InteractionResponseType(IntEnum):
18
+ PONG = 1
19
+ CHANNEL_MESSAGE_WITH_SOURCE = 4
20
+ DEFERRED_CHANNEL_MESSAGE_WITH_SOURCE = 5
21
+ DEFERRED_UPDATE_MESSAGE = 6
22
+ UPDATE_MESSAGE = 7
23
+ APPLICATION_COMMAND_AUTOCOMPLETE_RESULT = 8
24
+ MODAL = 9
25
+
26
+
27
+ class DiscordUser(TypedDict, total=False):
28
+ id: str
29
+ username: str
30
+ discriminator: str
31
+ avatar: Optional[str]
32
+ bot: bool
33
+ global_name: Optional[str]
34
+
35
+
36
+ class DiscordMember(TypedDict, total=False):
37
+ user: DiscordUser
38
+ nick: Optional[str]
39
+ roles: List[str]
40
+ joined_at: str
41
+ permissions: str
42
+
43
+
44
+ class DiscordMessage(TypedDict, total=False):
45
+ id: str
46
+ channel_id: str
47
+ content: str
48
+ author: DiscordUser
49
+ embeds: List[Dict[str, Any]]
50
+ components: List[Dict[str, Any]]
51
+
52
+
53
+ class InteractionData(TypedDict, total=False):
54
+ id: str
55
+ name: str
56
+ type: int
57
+ options: List[Dict[str, Any]]
58
+ custom_id: str
59
+ component_type: int
60
+ values: List[str]
61
+ components: List[Dict[str, Any]]
62
+
63
+
64
+ class Interaction(TypedDict, total=False):
65
+ id: str
66
+ application_id: str
67
+ type: int
68
+ data: InteractionData
69
+ guild_id: str
70
+ channel_id: str
71
+ member: DiscordMember
72
+ user: DiscordUser
73
+ token: str
74
+ version: int
75
+ message: DiscordMessage
76
+ locale: str