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.
loopbot/__init__.py ADDED
@@ -0,0 +1,60 @@
1
+ """
2
+ Loop Discord SDK for Python
3
+ Build Discord bots without websockets using HTTP Interactions
4
+ """
5
+
6
+ from .bot import Bot
7
+ from .client import Client
8
+ from .builders import (
9
+ EmbedBuilder,
10
+ ButtonBuilder,
11
+ ButtonStyle,
12
+ ActionRowBuilder,
13
+ SelectMenuBuilder,
14
+ ModalBuilder,
15
+ ContainerBuilder,
16
+ SeparatorBuilder,
17
+ TextDisplayBuilder,
18
+ MediaGalleryBuilder,
19
+ SectionBuilder,
20
+ FileBuilder,
21
+ )
22
+ from .context import (
23
+ CommandContext,
24
+ ButtonContext,
25
+ ModalContext,
26
+ SelectContext,
27
+ )
28
+ from .types import (
29
+ InteractionType,
30
+ InteractionResponseType,
31
+ )
32
+
33
+ __version__ = "1.0.0"
34
+ __all__ = [
35
+ # Core
36
+ "Bot",
37
+ "Client",
38
+ # Builders
39
+ "EmbedBuilder",
40
+ "ButtonBuilder",
41
+ "ButtonStyle",
42
+ "ActionRowBuilder",
43
+ "SelectMenuBuilder",
44
+ "ModalBuilder",
45
+ # Components V2
46
+ "ContainerBuilder",
47
+ "SeparatorBuilder",
48
+ "TextDisplayBuilder",
49
+ "MediaGalleryBuilder",
50
+ "SectionBuilder",
51
+ "FileBuilder",
52
+ # Context
53
+ "CommandContext",
54
+ "ButtonContext",
55
+ "ModalContext",
56
+ "SelectContext",
57
+ # Types
58
+ "InteractionType",
59
+ "InteractionResponseType",
60
+ ]
loopbot/bot.py ADDED
@@ -0,0 +1,584 @@
1
+ """
2
+ Bot class - Main entry point for the Loop Discord SDK
3
+ """
4
+
5
+ import asyncio
6
+ import signal
7
+ from typing import Any, Callable, Dict, List, Optional, Union
8
+
9
+ from .client import Client
10
+ from .context.command import CommandContext
11
+ from .context.button import ButtonContext
12
+ from .context.modal import ModalContext
13
+ from .context.select import SelectContext
14
+ from .types import InteractionType
15
+
16
+
17
+ CommandHandler = Callable[[CommandContext], None]
18
+ ButtonHandler = Callable[[ButtonContext], None]
19
+ ModalHandler = Callable[[ModalContext], None]
20
+ SelectHandler = Callable[[SelectContext], None]
21
+
22
+
23
+ class Command:
24
+ """Represents a slash command"""
25
+
26
+ def __init__(
27
+ self,
28
+ name: str,
29
+ description: str,
30
+ handler: CommandHandler,
31
+ options: Optional[List[Dict[str, Any]]] = None,
32
+ ):
33
+ self.name = name
34
+ self.description = description
35
+ self.handler = handler
36
+ self.options = options or []
37
+
38
+ def to_dict(self) -> Dict[str, Any]:
39
+ return {
40
+ "name": self.name,
41
+ "description": self.description,
42
+ "options": self.options,
43
+ }
44
+
45
+
46
+ class Bot:
47
+ """Main class for creating a Loop Discord bot"""
48
+
49
+ def __init__(
50
+ self,
51
+ token: str,
52
+ api_url: str = "https://api.loopbot.app",
53
+ ):
54
+ self.token = token
55
+ self.api_url = api_url
56
+ self._client = Client(token, api_url)
57
+ self._commands: Dict[str, Command] = {}
58
+ self._button_handlers: Dict[str, ButtonHandler] = {}
59
+ self._modal_handlers: Dict[str, ModalHandler] = {}
60
+ self._select_handlers: Dict[str, SelectHandler] = {}
61
+ self._application_id: str = ""
62
+ self._running = False
63
+
64
+ def command(
65
+ self,
66
+ name: str,
67
+ description: str,
68
+ options: Optional[List[Dict[str, Any]]] = None,
69
+ ) -> Callable[[CommandHandler], CommandHandler]:
70
+ """Decorator to register a command handler"""
71
+ def decorator(func: CommandHandler) -> CommandHandler:
72
+ self._commands[name] = Command(name, description, func, options)
73
+ return func
74
+ return decorator
75
+
76
+ def on_button(self, custom_id: str) -> Callable[[ButtonHandler], ButtonHandler]:
77
+ """Decorator to register a button handler"""
78
+ def decorator(func: ButtonHandler) -> ButtonHandler:
79
+ self._button_handlers[custom_id] = func
80
+ return func
81
+ return decorator
82
+
83
+ def on_modal(self, custom_id: str) -> Callable[[ModalHandler], ModalHandler]:
84
+ """Decorator to register a modal handler"""
85
+ def decorator(func: ModalHandler) -> ModalHandler:
86
+ self._modal_handlers[custom_id] = func
87
+ return func
88
+ return decorator
89
+
90
+ def on_select(self, custom_id: str) -> Callable[[SelectHandler], SelectHandler]:
91
+ """Decorator to register a select menu handler"""
92
+ def decorator(func: SelectHandler) -> SelectHandler:
93
+ self._select_handlers[custom_id] = func
94
+ return func
95
+ return decorator
96
+
97
+ def _handle_interaction(self, interaction: Dict[str, Any]) -> None:
98
+ """Handle incoming interaction"""
99
+ interaction_type = interaction.get("type")
100
+ data = interaction.get("data", {})
101
+
102
+ response: Optional[Dict[str, Any]] = None
103
+
104
+ if interaction_type == InteractionType.APPLICATION_COMMAND:
105
+ command_name = data.get("name", "")
106
+ command = self._commands.get(command_name)
107
+
108
+ if command:
109
+ ctx = CommandContext(interaction, self._client, self._application_id)
110
+ command.handler(ctx)
111
+ response = ctx.response
112
+
113
+ elif interaction_type == InteractionType.MESSAGE_COMPONENT:
114
+ custom_id = data.get("custom_id", "")
115
+ component_type = data.get("component_type")
116
+
117
+ # Button (type 2)
118
+ if component_type == 2:
119
+ handler = self._button_handlers.get(custom_id)
120
+ if handler:
121
+ ctx = ButtonContext(interaction, self._client, self._application_id)
122
+ handler(ctx)
123
+ response = ctx.response
124
+
125
+ # Select Menu (type 3)
126
+ elif component_type == 3:
127
+ handler = self._select_handlers.get(custom_id)
128
+ if handler:
129
+ ctx = SelectContext(interaction, self._client, self._application_id)
130
+ handler(ctx)
131
+ response = ctx.response
132
+
133
+ elif interaction_type == InteractionType.MODAL_SUBMIT:
134
+ custom_id = data.get("custom_id", "")
135
+ handler = self._modal_handlers.get(custom_id)
136
+
137
+ if handler:
138
+ ctx = ModalContext(interaction, self._client, self._application_id)
139
+ handler(ctx)
140
+ response = ctx.response
141
+
142
+ # Send response
143
+ if response:
144
+ asyncio.create_task(
145
+ self._client.respond(interaction.get("id", ""), response)
146
+ )
147
+
148
+ async def _start_async(self) -> None:
149
+ """Start the bot (async)"""
150
+ print("[Loop SDK] Starting bot...")
151
+
152
+ # Get command schemas
153
+ commands = [cmd.to_dict() for cmd in self._commands.values()]
154
+
155
+ # Connect to API
156
+ result = await self._client.connect(commands)
157
+ self._application_id = result.get("applicationId", "")
158
+
159
+ print(f"[Loop SDK] Connected to application {self._application_id}")
160
+ print(f"[Loop SDK] Deployed {len(commands)} commands")
161
+ print("[Loop SDK] Bot is running. Waiting for interactions...")
162
+
163
+ self._running = True
164
+
165
+ try:
166
+ # Start SSE connection
167
+ await self._client.connect_sse(self._handle_interaction)
168
+ except asyncio.CancelledError:
169
+ pass
170
+ finally:
171
+ await self.stop()
172
+
173
+ async def stop(self) -> None:
174
+ """Stop the bot"""
175
+ if self._running:
176
+ print("[Loop SDK] Stopping bot...")
177
+ self._running = False
178
+ await self._client.disconnect()
179
+
180
+ def start(self) -> None:
181
+ """Start the bot (blocking)"""
182
+ loop = asyncio.new_event_loop()
183
+ asyncio.set_event_loop(loop)
184
+
185
+ # Handle signals
186
+ def signal_handler() -> None:
187
+ loop.create_task(self.stop())
188
+
189
+ try:
190
+ loop.add_signal_handler(signal.SIGINT, signal_handler)
191
+ loop.add_signal_handler(signal.SIGTERM, signal_handler)
192
+ except NotImplementedError:
193
+ # Windows doesn't support add_signal_handler
194
+ pass
195
+
196
+ try:
197
+ loop.run_until_complete(self._start_async())
198
+ except KeyboardInterrupt:
199
+ loop.run_until_complete(self.stop())
200
+ finally:
201
+ loop.close()
202
+
203
+ def run(self) -> None:
204
+ """Alias for start()"""
205
+ self.start()
206
+
207
+ # ==================== MESSAGING ====================
208
+
209
+ async def send(
210
+ self,
211
+ channel_id: str,
212
+ content: Optional[str] = None,
213
+ embeds: Optional[List[Dict[str, Any]]] = None,
214
+ components: Optional[List[Dict[str, Any]]] = None,
215
+ ) -> Dict[str, Any]:
216
+ """Send a message to a channel"""
217
+ return await self._client.send_message(
218
+ self._application_id, channel_id, content, embeds, components
219
+ )
220
+
221
+ async def edit_message(
222
+ self,
223
+ channel_id: str,
224
+ message_id: str,
225
+ content: Optional[str] = None,
226
+ embeds: Optional[List[Dict[str, Any]]] = None,
227
+ components: Optional[List[Dict[str, Any]]] = None,
228
+ ) -> Dict[str, Any]:
229
+ """Edit a message"""
230
+ return await self._client.edit_message(
231
+ self._application_id, channel_id, message_id, content, embeds, components
232
+ )
233
+
234
+ async def delete_message(self, channel_id: str, message_id: str) -> None:
235
+ """Delete a message"""
236
+ await self._client.delete_message(self._application_id, channel_id, message_id)
237
+
238
+ async def get_messages(
239
+ self,
240
+ channel_id: str,
241
+ limit: Optional[int] = None,
242
+ before: Optional[str] = None,
243
+ after: Optional[str] = None,
244
+ ) -> List[Dict[str, Any]]:
245
+ """Get messages from a channel"""
246
+ return await self._client.get_messages(
247
+ self._application_id, channel_id, limit, before, after
248
+ )
249
+
250
+ async def get_message(self, channel_id: str, message_id: str) -> Dict[str, Any]:
251
+ """Get a single message"""
252
+ return await self._client.get_message(
253
+ self._application_id, channel_id, message_id
254
+ )
255
+
256
+ # ==================== CHANNELS ====================
257
+
258
+ async def get_channel(self, channel_id: str) -> Dict[str, Any]:
259
+ """Get a channel"""
260
+ return await self._client.get_channel(self._application_id, channel_id)
261
+
262
+ async def create_channel(
263
+ self,
264
+ guild_id: str,
265
+ name: str,
266
+ channel_type: Optional[int] = None,
267
+ topic: Optional[str] = None,
268
+ permission_overwrites: Optional[List[Dict[str, Any]]] = None,
269
+ parent_id: Optional[str] = None,
270
+ ) -> Dict[str, Any]:
271
+ """Create a channel in a guild"""
272
+ return await self._client.create_channel(
273
+ self._application_id, guild_id, name, channel_type, topic,
274
+ permission_overwrites, parent_id
275
+ )
276
+
277
+ async def modify_channel(
278
+ self,
279
+ channel_id: str,
280
+ name: Optional[str] = None,
281
+ topic: Optional[str] = None,
282
+ permission_overwrites: Optional[List[Dict[str, Any]]] = None,
283
+ ) -> Dict[str, Any]:
284
+ """Modify a channel"""
285
+ return await self._client.modify_channel(
286
+ self._application_id, channel_id, name, topic, permission_overwrites
287
+ )
288
+
289
+ async def delete_channel(self, channel_id: str) -> None:
290
+ """Delete a channel"""
291
+ await self._client.delete_channel(self._application_id, channel_id)
292
+
293
+ # ==================== GUILDS ====================
294
+
295
+ async def get_guild(self, guild_id: str) -> Dict[str, Any]:
296
+ """Get guild info"""
297
+ return await self._client.get_guild(self._application_id, guild_id)
298
+
299
+ async def get_guild_channels(self, guild_id: str) -> List[Dict[str, Any]]:
300
+ """Get guild channels"""
301
+ return await self._client.get_guild_channels(self._application_id, guild_id)
302
+
303
+ async def get_guild_roles(self, guild_id: str) -> List[Dict[str, Any]]:
304
+ """Get guild roles"""
305
+ return await self._client.get_guild_roles(self._application_id, guild_id)
306
+
307
+ # ==================== MEMBERS ====================
308
+
309
+ async def get_guild_member(self, guild_id: str, user_id: str) -> Dict[str, Any]:
310
+ """Get a guild member"""
311
+ return await self._client.get_guild_member(
312
+ self._application_id, guild_id, user_id
313
+ )
314
+
315
+ async def list_guild_members(
316
+ self,
317
+ guild_id: str,
318
+ limit: Optional[int] = None,
319
+ after: Optional[str] = None,
320
+ ) -> List[Dict[str, Any]]:
321
+ """List guild members"""
322
+ return await self._client.list_guild_members(
323
+ self._application_id, guild_id, limit, after
324
+ )
325
+
326
+ async def add_member_role(
327
+ self, guild_id: str, user_id: str, role_id: str
328
+ ) -> None:
329
+ """Add role to member"""
330
+ await self._client.add_member_role(
331
+ self._application_id, guild_id, user_id, role_id
332
+ )
333
+
334
+ async def remove_member_role(
335
+ self, guild_id: str, user_id: str, role_id: str
336
+ ) -> None:
337
+ """Remove role from member"""
338
+ await self._client.remove_member_role(
339
+ self._application_id, guild_id, user_id, role_id
340
+ )
341
+
342
+ async def kick_member(self, guild_id: str, user_id: str) -> None:
343
+ """Kick member"""
344
+ await self._client.kick_member(self._application_id, guild_id, user_id)
345
+
346
+ async def ban_member(
347
+ self,
348
+ guild_id: str,
349
+ user_id: str,
350
+ delete_message_seconds: Optional[int] = None,
351
+ ) -> None:
352
+ """Ban member"""
353
+ await self._client.ban_member(
354
+ self._application_id, guild_id, user_id, delete_message_seconds
355
+ )
356
+
357
+ async def unban_member(self, guild_id: str, user_id: str) -> None:
358
+ """Unban member"""
359
+ await self._client.unban_member(self._application_id, guild_id, user_id)
360
+
361
+ # ==================== REACTIONS ====================
362
+
363
+ async def add_reaction(
364
+ self, channel_id: str, message_id: str, emoji: str
365
+ ) -> None:
366
+ """Add reaction to message"""
367
+ await self._client.add_reaction(
368
+ self._application_id, channel_id, message_id, emoji
369
+ )
370
+
371
+ async def remove_reaction(
372
+ self, channel_id: str, message_id: str, emoji: str
373
+ ) -> None:
374
+ """Remove reaction from message"""
375
+ await self._client.remove_reaction(
376
+ self._application_id, channel_id, message_id, emoji
377
+ )
378
+
379
+ # ==================== PINS ====================
380
+
381
+ async def pin_message(self, channel_id: str, message_id: str) -> None:
382
+ """Pin a message"""
383
+ await self._client.pin_message(
384
+ self._application_id, channel_id, message_id
385
+ )
386
+
387
+ async def unpin_message(self, channel_id: str, message_id: str) -> None:
388
+ """Unpin a message"""
389
+ await self._client.unpin_message(
390
+ self._application_id, channel_id, message_id
391
+ )
392
+
393
+ async def get_pinned_messages(self, channel_id: str) -> List[Dict[str, Any]]:
394
+ """Get pinned messages"""
395
+ return await self._client.get_pinned_messages(
396
+ self._application_id, channel_id
397
+ )
398
+
399
+ # ==================== USERS ====================
400
+
401
+ async def get_user(self, user_id: str) -> Dict[str, Any]:
402
+ """Get user info"""
403
+ return await self._client.get_user(self._application_id, user_id)
404
+
405
+ # ==================== THREADS ====================
406
+
407
+ async def create_thread(
408
+ self,
409
+ channel_id: str,
410
+ name: str,
411
+ message_id: Optional[str] = None,
412
+ thread_type: Optional[int] = None,
413
+ auto_archive_duration: Optional[int] = None,
414
+ ) -> Dict[str, Any]:
415
+ """Create a thread"""
416
+ return await self._client.create_thread(
417
+ self._application_id, channel_id, name, message_id,
418
+ thread_type, auto_archive_duration
419
+ )
420
+
421
+ # ==================== FORUM CHANNELS ====================
422
+
423
+ async def create_forum_post(
424
+ self,
425
+ channel_id: str,
426
+ name: str,
427
+ message: Dict[str, Any],
428
+ applied_tags: Optional[List[str]] = None,
429
+ ) -> Dict[str, Any]:
430
+ """Create a forum post"""
431
+ return await self._client.create_forum_post(
432
+ self._application_id, channel_id, name, message, applied_tags
433
+ )
434
+
435
+ async def get_forum_tags(self, channel_id: str) -> List[Dict[str, Any]]:
436
+ """Get forum tags"""
437
+ return await self._client.get_forum_tags(self._application_id, channel_id)
438
+
439
+ async def modify_forum_tags(
440
+ self, channel_id: str, tags: List[Dict[str, Any]]
441
+ ) -> Dict[str, Any]:
442
+ """Modify forum tags"""
443
+ return await self._client.modify_forum_tags(
444
+ self._application_id, channel_id, tags
445
+ )
446
+
447
+ async def archive_thread(
448
+ self, thread_id: str, archived: bool = True
449
+ ) -> Dict[str, Any]:
450
+ """Archive thread"""
451
+ return await self._client.archive_thread(
452
+ self._application_id, thread_id, archived
453
+ )
454
+
455
+ async def lock_thread(
456
+ self, thread_id: str, locked: bool = True
457
+ ) -> Dict[str, Any]:
458
+ """Lock thread"""
459
+ return await self._client.lock_thread(
460
+ self._application_id, thread_id, locked
461
+ )
462
+
463
+ # ==================== ROLES ====================
464
+
465
+ async def get_roles(self, guild_id: str) -> List[Dict[str, Any]]:
466
+ """Get roles"""
467
+ return await self._client.get_roles(self._application_id, guild_id)
468
+
469
+ async def create_role(
470
+ self,
471
+ guild_id: str,
472
+ name: Optional[str] = None,
473
+ permissions: Optional[str] = None,
474
+ color: Optional[int] = None,
475
+ hoist: Optional[bool] = None,
476
+ mentionable: Optional[bool] = None,
477
+ ) -> Dict[str, Any]:
478
+ """Create role"""
479
+ return await self._client.create_role(
480
+ self._application_id, guild_id, name, permissions, color, hoist, mentionable
481
+ )
482
+
483
+ async def modify_role(
484
+ self,
485
+ guild_id: str,
486
+ role_id: str,
487
+ name: Optional[str] = None,
488
+ permissions: Optional[str] = None,
489
+ color: Optional[int] = None,
490
+ hoist: Optional[bool] = None,
491
+ mentionable: Optional[bool] = None,
492
+ ) -> Dict[str, Any]:
493
+ """Modify role"""
494
+ return await self._client.modify_role(
495
+ self._application_id, guild_id, role_id, name, permissions, color, hoist, mentionable
496
+ )
497
+
498
+ async def delete_role(self, guild_id: str, role_id: str) -> None:
499
+ """Delete role"""
500
+ await self._client.delete_role(self._application_id, guild_id, role_id)
501
+
502
+ async def reorder_roles(
503
+ self, guild_id: str, positions: List[Dict[str, Any]]
504
+ ) -> List[Dict[str, Any]]:
505
+ """Reorder roles"""
506
+ return await self._client.reorder_roles(
507
+ self._application_id, guild_id, positions
508
+ )
509
+
510
+ # ==================== WEBHOOKS ====================
511
+
512
+ async def create_webhook(
513
+ self, channel_id: str, name: str, avatar: Optional[str] = None
514
+ ) -> Dict[str, Any]:
515
+ """Create webhook"""
516
+ return await self._client.create_webhook(
517
+ self._application_id, channel_id, name, avatar
518
+ )
519
+
520
+ async def get_channel_webhooks(self, channel_id: str) -> List[Dict[str, Any]]:
521
+ """Get channel webhooks"""
522
+ return await self._client.get_channel_webhooks(
523
+ self._application_id, channel_id
524
+ )
525
+
526
+ async def get_guild_webhooks(self, guild_id: str) -> List[Dict[str, Any]]:
527
+ """Get guild webhooks"""
528
+ return await self._client.get_guild_webhooks(self._application_id, guild_id)
529
+
530
+ async def get_webhook(self, webhook_id: str) -> Dict[str, Any]:
531
+ """Get webhook"""
532
+ return await self._client.get_webhook(self._application_id, webhook_id)
533
+
534
+ async def modify_webhook(
535
+ self,
536
+ webhook_id: str,
537
+ name: Optional[str] = None,
538
+ avatar: Optional[str] = None,
539
+ channel_id: Optional[str] = None,
540
+ ) -> Dict[str, Any]:
541
+ """Modify webhook"""
542
+ return await self._client.modify_webhook(
543
+ self._application_id, webhook_id, name, avatar, channel_id
544
+ )
545
+
546
+ async def delete_webhook(self, webhook_id: str) -> None:
547
+ """Delete webhook"""
548
+ await self._client.delete_webhook(self._application_id, webhook_id)
549
+
550
+ async def execute_webhook(
551
+ self,
552
+ webhook_id: str,
553
+ webhook_token: str,
554
+ content: Optional[str] = None,
555
+ username: Optional[str] = None,
556
+ avatar_url: Optional[str] = None,
557
+ embeds: Optional[List[Dict[str, Any]]] = None,
558
+ wait: bool = False,
559
+ ) -> Optional[Dict[str, Any]]:
560
+ """Execute webhook (send message)"""
561
+ return await self._client.execute_webhook(
562
+ webhook_id, webhook_token, content, username, avatar_url, embeds, wait
563
+ )
564
+
565
+ async def edit_webhook_message(
566
+ self,
567
+ webhook_id: str,
568
+ webhook_token: str,
569
+ message_id: str,
570
+ content: Optional[str] = None,
571
+ embeds: Optional[List[Dict[str, Any]]] = None,
572
+ ) -> Dict[str, Any]:
573
+ """Edit webhook message"""
574
+ return await self._client.edit_webhook_message(
575
+ webhook_id, webhook_token, message_id, content, embeds
576
+ )
577
+
578
+ async def delete_webhook_message(
579
+ self, webhook_id: str, webhook_token: str, message_id: str
580
+ ) -> None:
581
+ """Delete webhook message"""
582
+ await self._client.delete_webhook_message(
583
+ webhook_id, webhook_token, message_id
584
+ )
@@ -0,0 +1,30 @@
1
+ """
2
+ Builders for Discord components
3
+ """
4
+
5
+ from .embed import EmbedBuilder
6
+ from .button import ButtonBuilder, ButtonStyle
7
+ from .action_row import ActionRowBuilder
8
+ from .select_menu import SelectMenuBuilder
9
+ from .modal import ModalBuilder
10
+ from .container import ContainerBuilder
11
+ from .separator import SeparatorBuilder
12
+ from .text_display import TextDisplayBuilder
13
+ from .media_gallery import MediaGalleryBuilder
14
+ from .section import SectionBuilder
15
+ from .file import FileBuilder
16
+
17
+ __all__ = [
18
+ "EmbedBuilder",
19
+ "ButtonBuilder",
20
+ "ButtonStyle",
21
+ "ActionRowBuilder",
22
+ "SelectMenuBuilder",
23
+ "ModalBuilder",
24
+ "ContainerBuilder",
25
+ "SeparatorBuilder",
26
+ "TextDisplayBuilder",
27
+ "MediaGalleryBuilder",
28
+ "SectionBuilder",
29
+ "FileBuilder",
30
+ ]