aiko-api 0.1.1__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.
@@ -0,0 +1,78 @@
1
+ Metadata-Version: 2.4
2
+ Name: aiko-api
3
+ Version: 0.1.1
4
+ Summary: A clean, efficient Discord API wrapper for bot development.
5
+ Author: Aiko
6
+ License: MIT
7
+ Classifier: Programming Language :: Python :: 3
8
+ Classifier: License :: OSI Approved :: MIT License
9
+ Classifier: Operating System :: OS Independent
10
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
11
+ Classifier: Intended Audience :: Developers
12
+ Requires-Python: >=3.10
13
+ Description-Content-Type: text/markdown
14
+ Requires-Dist: curl_cffi>=0.7.0
15
+ Requires-Dist: aiohttp>=3.9.0
16
+ Requires-Dist: uvloop>=0.19.0; sys_platform != "win32"
17
+ Requires-Dist: attrs>=23.2.0
18
+
19
+ # aiko_api
20
+
21
+ A clean, efficient, and modern Discord API wrapper for Python.
22
+
23
+ `aiko_api` is designed to be a lightweight and easy-to-use alternative for building Discord bots and selfbots. It features a flat command hierarchy, smart signatures, and a powerful plugin system with hot-reloading.
24
+
25
+ ## Features
26
+
27
+ - **Flat Sub-command Hierarchy:** Decorate commands with their parent commands for clean nesting.
28
+ - **Smart Signatures:** `self` is optional in Cogs; the library detects if it's needed.
29
+ - **Clean Cog Events:** Automatically register `on_` methods as event listeners.
30
+ - **Advanced Plugin System:** Load Cogs and standalone commands from directories with built-in hot-reloading.
31
+ - **Optimized HTTP:** Built on top of `curl_cffi` for high performance and better impersonation.
32
+
33
+ ## Installation
34
+
35
+ ```bash
36
+ pip install aiko_api
37
+ ```
38
+
39
+ ## Quick Start
40
+
41
+ ```python
42
+ import asyncio
43
+ from aiko_api import Bot
44
+
45
+ bot = Bot(command_prefix="!")
46
+
47
+ @bot.command()
48
+ async def ping(ctx):
49
+ await ctx.reply("Pong! 🏓")
50
+
51
+ @bot.event
52
+ async def on_ready(user):
53
+ print(f"Logged in as {user.username}")
54
+
55
+ if __name__ == "__main__":
56
+ asyncio.run(bot.start("YOUR_TOKEN"))
57
+ ```
58
+
59
+ ## Advanced Usage: Cogs and Sub-commands
60
+
61
+ ```python
62
+ from aiko_api import Cog, command
63
+
64
+ class Moderation(Cog):
65
+ @command()
66
+ async def config(self, ctx):
67
+ await ctx.send("Main config menu.")
68
+
69
+ @config
70
+ async def prefix(self, ctx, new_prefix: str):
71
+ await ctx.send(f"Prefix updated to {new_prefix}")
72
+
73
+ bot.add_cog(Moderation(bot))
74
+ ```
75
+
76
+ ## License
77
+
78
+ This project is licensed under the MIT License.
@@ -0,0 +1,60 @@
1
+ # aiko_api
2
+
3
+ A clean, efficient, and modern Discord API wrapper for Python.
4
+
5
+ `aiko_api` is designed to be a lightweight and easy-to-use alternative for building Discord bots and selfbots. It features a flat command hierarchy, smart signatures, and a powerful plugin system with hot-reloading.
6
+
7
+ ## Features
8
+
9
+ - **Flat Sub-command Hierarchy:** Decorate commands with their parent commands for clean nesting.
10
+ - **Smart Signatures:** `self` is optional in Cogs; the library detects if it's needed.
11
+ - **Clean Cog Events:** Automatically register `on_` methods as event listeners.
12
+ - **Advanced Plugin System:** Load Cogs and standalone commands from directories with built-in hot-reloading.
13
+ - **Optimized HTTP:** Built on top of `curl_cffi` for high performance and better impersonation.
14
+
15
+ ## Installation
16
+
17
+ ```bash
18
+ pip install aiko_api
19
+ ```
20
+
21
+ ## Quick Start
22
+
23
+ ```python
24
+ import asyncio
25
+ from aiko_api import Bot
26
+
27
+ bot = Bot(command_prefix="!")
28
+
29
+ @bot.command()
30
+ async def ping(ctx):
31
+ await ctx.reply("Pong! 🏓")
32
+
33
+ @bot.event
34
+ async def on_ready(user):
35
+ print(f"Logged in as {user.username}")
36
+
37
+ if __name__ == "__main__":
38
+ asyncio.run(bot.start("YOUR_TOKEN"))
39
+ ```
40
+
41
+ ## Advanced Usage: Cogs and Sub-commands
42
+
43
+ ```python
44
+ from aiko_api import Cog, command
45
+
46
+ class Moderation(Cog):
47
+ @command()
48
+ async def config(self, ctx):
49
+ await ctx.send("Main config menu.")
50
+
51
+ @config
52
+ async def prefix(self, ctx, new_prefix: str):
53
+ await ctx.send(f"Prefix updated to {new_prefix}")
54
+
55
+ bot.add_cog(Moderation(bot))
56
+ ```
57
+
58
+ ## License
59
+
60
+ This project is licensed under the MIT License.
@@ -0,0 +1,11 @@
1
+ __title__ = 'aiko_api'
2
+ __author__ = 'Aiko'
3
+ __license__ = 'MIT'
4
+ __version__ = '0.1.0'
5
+
6
+ from .client import Client
7
+ from .common.errors import *
8
+ from .common.models import User, Message, Guild, Channel, Member, Activity
9
+ from .common.constants import ActivityType, OpCode
10
+ from .commands import Bot, Context, Command, Cog, command, cog
11
+ from .katlog import get_logger
File without changes
@@ -0,0 +1,61 @@
1
+ # aiko_api/actions/guilds.py
2
+ from typing import Union, Optional
3
+ import datetime
4
+
5
+ class GuildActions:
6
+ def __init__(self, state):
7
+ self._state = state
8
+ self._http = state.http
9
+
10
+ async def leave(self, guild_id: Union[int, str]):
11
+ """Leaves a guild."""
12
+ return await self._http.delete(f"/users/@me/guilds/{guild_id}")
13
+
14
+ async def get_guild(self, guild_id: Union[int, str]):
15
+ """Fetches guild information."""
16
+ return await self._http.get(f"/guilds/{guild_id}")
17
+
18
+ async def get_channels(self, guild_id: Union[int, str]):
19
+ """Fetches channels in a guild."""
20
+ return await self._http.get(f"/guilds/{guild_id}/channels")
21
+
22
+ async def kick(self, guild_id: Union[int, str], user_id: Union[int, str], reason: Optional[str] = None):
23
+ """Kicks a member from the guild."""
24
+ headers = {}
25
+ if reason:
26
+ from urllib.parse import quote
27
+ headers["X-Audit-Log-Reason"] = quote(reason)
28
+ return await self._http.delete(f"/guilds/{guild_id}/members/{user_id}", headers=headers)
29
+
30
+ async def ban(self, guild_id: Union[int, str], user_id: Union[int, str], delete_message_days: int = 0, reason: Optional[str] = None):
31
+ """Bans a member from the guild."""
32
+ headers = {}
33
+ if reason:
34
+ from urllib.parse import quote
35
+ headers["X-Audit-Log-Reason"] = quote(reason)
36
+
37
+ payload = {"delete_message_days": delete_message_days}
38
+ return await self._http.put(f"/guilds/{guild_id}/bans/{user_id}", json=payload, headers=headers)
39
+
40
+ async def unban(self, guild_id: Union[int, str], user_id: Union[int, str], reason: Optional[str] = None):
41
+ """Unbans a member from the guild."""
42
+ headers = {}
43
+ if reason:
44
+ from urllib.parse import quote
45
+ headers["X-Audit-Log-Reason"] = quote(reason)
46
+ return await self._http.delete(f"/guilds/{guild_id}/bans/{user_id}", headers=headers)
47
+
48
+ async def edit_member(self, guild_id: Union[int, str], user_id: Union[int, str], payload: dict, reason: Optional[str] = None):
49
+ """Edits a member (nick, roles, mute, deaf, communication_disabled_until)."""
50
+ headers = {}
51
+ if reason:
52
+ from urllib.parse import quote
53
+ headers["X-Audit-Log-Reason"] = quote(reason)
54
+ return await self._http.patch(f"/guilds/{guild_id}/members/{user_id}", json=payload, headers=headers)
55
+
56
+ async def timeout(self, guild_id: Union[int, str], user_id: Union[int, str], until: Optional[datetime.datetime], reason: Optional[str] = None):
57
+ """Timeouts a member until a specific datetime (ISO8601). Set until to None to remove timeout."""
58
+ payload = {
59
+ "communication_disabled_until": until.isoformat() if until else None
60
+ }
61
+ return await self.edit_member(guild_id, user_id, payload, reason=reason)
@@ -0,0 +1,60 @@
1
+ # aiko_api/actions/messages.py
2
+ import json
3
+ import os
4
+ from typing import Optional, List, Any, Union, Tuple
5
+
6
+ class MessageActions:
7
+ def __init__(self, state):
8
+ self._state = state
9
+ self._http = state.http
10
+
11
+ async def send(
12
+ self,
13
+ channel_id: Union[int, str],
14
+ content: str,
15
+ files: Optional[List[Union[str, Tuple[str, Any]]]] = None,
16
+ message_reference: Optional[dict] = None
17
+ ):
18
+ payload = {"content": content}
19
+ if message_reference:
20
+ payload["message_reference"] = message_reference
21
+
22
+ if files:
23
+ file_data = {}
24
+ for i, item in enumerate(files):
25
+ if isinstance(item, tuple):
26
+ filename, fp = item
27
+ file_data[f"file{i}"] = (filename, fp)
28
+ else:
29
+ file_data[f"file{i}"] = item
30
+
31
+ return await self._http.post(
32
+ f"/channels/{channel_id}/messages",
33
+ json=payload,
34
+ files=file_data
35
+ )
36
+
37
+ return await self._http.post(f"/channels/{channel_id}/messages", json=payload)
38
+
39
+ async def reply(self, channel_id, message_id, content, **kwargs):
40
+ return await self.send(
41
+ channel_id,
42
+ content,
43
+ message_reference={"message_id": str(message_id)},
44
+ **kwargs
45
+ )
46
+
47
+ async def edit(self, channel_id, message_id, content):
48
+ return await self._http.patch(
49
+ f"/channels/{channel_id}/messages/{message_id}",
50
+ json={"content": content}
51
+ )
52
+
53
+ async def delete(self, channel_id, message_id):
54
+ return await self._http.delete(f"/channels/{channel_id}/messages/{message_id}")
55
+
56
+ async def add_reaction(self, channel_id, message_id, emoji):
57
+ from urllib.parse import quote
58
+ return await self._http.put(
59
+ f"/channels/{channel_id}/messages/{message_id}/reactions/{quote(emoji)}/@me"
60
+ )
@@ -0,0 +1,71 @@
1
+ # aiko_api/actions/user.py
2
+ import re
3
+ import io
4
+ from typing import Optional
5
+ from curl_cffi import requests
6
+
7
+ class UserActions:
8
+ def __init__(self, state):
9
+ self._state = state
10
+ self._http = state.http
11
+
12
+ async def static_login(self, token):
13
+ return await self._http.get("/users/@me")
14
+
15
+ async def edit_profile(self, payload):
16
+ return await self._http.patch("/users/@me", json=payload)
17
+
18
+ async def edit_settings(self, payload):
19
+ return await self._http.patch("/users/@me/settings", json=payload)
20
+
21
+ async def create_dm(self, recipient_id):
22
+ return await self._http.post("/users/@me/channels", json={"recipient_id": recipient_id})
23
+
24
+ async def get_relationships(self):
25
+ return await self._http.get("/users/@me/relationships")
26
+
27
+ async def add_friend(self, user_id):
28
+ return await self._http.put(f"/users/@me/relationships/{user_id}", json={})
29
+
30
+ async def remove_relationship(self, user_id):
31
+ return await self._http.delete(f"/users/@me/relationships/{user_id}")
32
+
33
+ async def block_user(self, user_id):
34
+ return await self._http.put(f"/users/@me/relationships/{user_id}", json={"type": 2})
35
+
36
+ async def change_presence(self, activity=None, status="online", since=0, afk=False):
37
+ if activity and activity.assets:
38
+ for key in ("large_image", "small_image"):
39
+ if activity.assets.get(key) and activity.assets[key].startswith("http"):
40
+ new_asset = await self.upload_asset(activity.assets[key])
41
+ if new_asset: activity.assets[key] = new_asset
42
+
43
+ if self._state.ws:
44
+ await self._state.ws.change_presence(activity, status, since, afk)
45
+
46
+ async def upload_asset(self, image_url: str) -> Optional[str]:
47
+ discord_cdn_pattern = r"https?://(?:cdn\.discordapp\.com|media\.discordapp\.net)/attachments/(\d+)/(\d+)/(.+)"
48
+ match = re.search(discord_cdn_pattern, image_url)
49
+ if match:
50
+ channel_id, attachment_id, filename = match.groups()
51
+ return f"mp:attachments/{channel_id}/{attachment_id}/{filename}"
52
+
53
+ try:
54
+ async with requests.AsyncSession() as session:
55
+ r = await session.get(image_url, proxies={"http": self._http.proxy, "https": self._http.proxy} if self._http.proxy else None)
56
+ if r.status_code != 200: return None
57
+ image_bytes = r.content
58
+ filename = image_url.split("/")[-1].split("?")[0] or "asset.png"
59
+
60
+ dm_channel = await self.create_dm(self._state.user.id)
61
+ message = await self._state.messages.send(dm_channel["id"], "", files=[(filename, io.BytesIO(image_bytes))])
62
+
63
+ if not message.get("attachments"): return None
64
+ new_url = message["attachments"][0]["url"]
65
+ new_match = re.search(discord_cdn_pattern, new_url)
66
+ if not new_match: return None
67
+
68
+ cid, aid, fname = new_match.groups()
69
+ return f"mp:attachments/{cid}/{aid}/{fname}"
70
+ except Exception:
71
+ return None
@@ -0,0 +1,136 @@
1
+ from typing import Dict, Optional, Any, Deque, List
2
+ from collections import deque
3
+ from .common.models import User, Guild, Message, Channel, Member
4
+
5
+ class Cache:
6
+ def __init__(self, client):
7
+ self.client = client
8
+ self.users: Dict[str, User] = {}
9
+ self.guilds: Dict[str, Guild] = {}
10
+ self.channels: Dict[str, Channel] = {}
11
+ self.messages: Deque[Message] = deque(maxlen=1000)
12
+
13
+ def store_user(self, data: Dict[str, Any]) -> User:
14
+ user_id = data['id']
15
+ if user_id in self.users:
16
+ user = self.users[user_id]
17
+ user.username = data.get('username', user.username)
18
+ user.discriminator = data.get('discriminator', user.discriminator)
19
+ user.avatar = data.get('avatar', user.avatar)
20
+ return user
21
+
22
+ user = User(
23
+ id=user_id,
24
+ username=data['username'],
25
+ discriminator=data.get('discriminator', '0000'),
26
+ avatar=data.get('avatar'),
27
+ bot=data.get('bot', False),
28
+ _state=self.client
29
+ )
30
+ self.users[user.id] = user
31
+ return user
32
+
33
+ def get_user(self, user_id: str) -> Optional[User]:
34
+ return self.users.get(user_id)
35
+
36
+ def store_member(self, guild_id: str, data: Dict[str, Any]) -> Member:
37
+ user_data = data.get('user')
38
+ if not user_data:
39
+ user_data = data
40
+
41
+ user = self.store_user(user_data)
42
+ member = Member(
43
+ id=user.id,
44
+ username=user.username,
45
+ discriminator=user.discriminator,
46
+ avatar=user.avatar,
47
+ bot=user.bot,
48
+ nick=data.get('nick'),
49
+ roles=data.get('roles', []),
50
+ joined_at=data.get('joined_at'),
51
+ _state=self.client
52
+ )
53
+
54
+ guild = self.get_guild(guild_id)
55
+ if guild:
56
+ guild.members[member.id] = member
57
+
58
+ return member
59
+
60
+ def store_guild(self, data: Dict[str, Any]) -> Guild:
61
+ guild_id = data['id']
62
+ guild = self.guilds.get(guild_id)
63
+
64
+ if not guild:
65
+ guild = Guild(
66
+ id=guild_id,
67
+ name=data['name'],
68
+ icon=data.get('icon'),
69
+ owner_id=data.get('owner_id', ''),
70
+ _state=self.client
71
+ )
72
+ self.guilds[guild.id] = guild
73
+
74
+ guild.name = data.get('name', guild.name)
75
+ guild.icon = data.get('icon', guild.icon)
76
+ guild.owner_id = data.get('owner_id', guild.owner_id)
77
+
78
+ for channel_data in data.get('channels', []):
79
+ channel_data['guild_id'] = guild_id
80
+ self.store_channel(channel_data)
81
+
82
+ for member_data in data.get('members', []):
83
+ self.store_member(guild_id, member_data)
84
+
85
+ return guild
86
+
87
+ def get_guild(self, guild_id: str) -> Optional[Guild]:
88
+ return self.guilds.get(guild_id)
89
+
90
+ def store_channel(self, data: Dict[str, Any]) -> Channel:
91
+ channel_id = data['id']
92
+ channel = Channel(
93
+ id=channel_id,
94
+ name=data.get('name', 'unknown'),
95
+ type=data.get('type', 0),
96
+ guild_id=data.get('guild_id'),
97
+ _state=self.client
98
+ )
99
+ self.channels[channel.id] = channel
100
+
101
+ if channel.guild_id:
102
+ guild = self.get_guild(channel.guild_id)
103
+ if guild:
104
+ guild.channels[channel.id] = channel
105
+
106
+ return channel
107
+
108
+ def get_channel(self, channel_id: str) -> Optional[Channel]:
109
+ return self.channels.get(channel_id)
110
+
111
+ def store_message(self, data: Dict[str, Any]) -> Message:
112
+ author_data = data.get('author')
113
+ author = self.store_user(author_data) if author_data else None
114
+
115
+ message = Message(
116
+ id=data['id'],
117
+ channel_id=data['channel_id'],
118
+ guild_id=data.get('guild_id'),
119
+ author=author,
120
+ content=data.get('content', ''),
121
+ timestamp=data['timestamp'],
122
+ tts=data.get('tts', False),
123
+ mention_everyone=data.get('mention_everyone', False),
124
+ attachments=data.get('attachments', []),
125
+ embeds=data.get('embeds', []),
126
+ reactions=data.get('reactions', []),
127
+ _state=self.client
128
+ )
129
+ self.messages.append(message)
130
+ return message
131
+
132
+ def clear(self):
133
+ self.users.clear()
134
+ self.guilds.clear()
135
+ self.channels.clear()
136
+ self.messages.clear()
@@ -0,0 +1,125 @@
1
+ # aiko_api/client.py
2
+ import asyncio
3
+ from typing import Optional
4
+
5
+ try:
6
+ import uvloop
7
+ except ImportError:
8
+ uvloop = None
9
+
10
+ from .katlog import get_logger
11
+ from .core.http import HTTPClient
12
+ from .gateway import DiscordWebSocket
13
+ from .cache import Cache
14
+ from .actions.messages import MessageActions
15
+ from .actions.user import UserActions
16
+ from .actions.guilds import GuildActions
17
+
18
+ log = get_logger("aiko_api", level="DEBUG")
19
+
20
+ class Client:
21
+ def __init__(self, loop=None, proxy=None):
22
+ self.loop = loop
23
+ self.proxy = proxy
24
+ self.http = HTTPClient(proxy=proxy)
25
+
26
+ # Action components
27
+ self.messages = MessageActions(self)
28
+ self.user_actions = UserActions(self)
29
+ self.guilds = GuildActions(self)
30
+
31
+ self.ws = None
32
+ self._events = {}
33
+ self._event_map = {
34
+ "READY": "on_ready",
35
+ "MESSAGE_CREATE": "on_message_create",
36
+ "GUILD_CREATE": "on_guild_create",
37
+ }
38
+ self.user = None
39
+ self.cache = Cache(self)
40
+ self._waiters = {}
41
+ self._stop_event = asyncio.Event()
42
+
43
+ def event(self, coro):
44
+ if not asyncio.iscoroutinefunction(coro):
45
+ raise TypeError("event registered must be a coroutine function")
46
+ name = coro.__name__
47
+ if name not in self._events:
48
+ self._events[name] = []
49
+ self._events[name].append(coro)
50
+ return coro
51
+
52
+ async def dispatch(self, event, data):
53
+ method_name = self._event_map.get(event, f"on_{event.lower()}")
54
+
55
+ event_arg = data
56
+ if event == "MESSAGE_CREATE":
57
+ event_arg = self.cache.store_message(data)
58
+ elif event == "READY":
59
+ event_arg = self.user
60
+
61
+ # Handle waiters
62
+ if event.lower() in self._waiters:
63
+ for future, check in self._waiters[event.lower()][:]:
64
+ if not future.cancelled():
65
+ try:
66
+ if check(event_arg):
67
+ future.set_result(event_arg)
68
+ self._waiters[event.lower()].remove((future, check))
69
+ except Exception as e:
70
+ future.set_exception(e)
71
+ self._waiters[event.lower()].remove((future, check))
72
+
73
+ if method_name in self._events:
74
+ for coro in self._events[method_name]:
75
+ try:
76
+ await coro(event_arg)
77
+ except Exception as e:
78
+ log.error(f"Error in event {method_name}: {e}")
79
+
80
+ async def wait_for(self, event, check=None, timeout=None):
81
+ if not check: check = lambda *args: True
82
+ future = self.loop.create_future()
83
+ event = event.lower()
84
+ if event not in self._waiters: self._waiters[event] = []
85
+ self._waiters[event].append((future, check))
86
+ try: return await asyncio.wait_for(future, timeout=timeout)
87
+ except asyncio.TimeoutError:
88
+ if event in self._waiters: self._waiters[event].remove((future, check))
89
+ raise
90
+
91
+ async def start(self, token):
92
+ if self.loop is None: self.loop = asyncio.get_running_loop()
93
+ self.http.token = token
94
+ self.http.headers.token = token
95
+ await self.user_actions.static_login(token)
96
+ self.ws = DiscordWebSocket(self, self.loop)
97
+ await self.ws.connect()
98
+ await self._stop_event.wait()
99
+
100
+ def run(self, token):
101
+ if self.loop is None:
102
+ try: self.loop = asyncio.get_event_loop()
103
+ except RuntimeError:
104
+ self.loop = asyncio.new_event_loop()
105
+ asyncio.set_event_loop(self.loop)
106
+ if uvloop: asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
107
+ try: self.loop.run_until_complete(self.start(token))
108
+ except (KeyboardInterrupt, SystemExit): pass
109
+ finally:
110
+ self.loop.run_until_complete(self.close())
111
+ self.loop.close()
112
+
113
+ async def close(self):
114
+ self._stop_event.set()
115
+ if self.ws: await self.ws.close()
116
+ await self.http.close()
117
+
118
+ # Legacy/Shortcut methods
119
+ async def send_message(self, *args, **kwargs): return await self.messages.send(*args, **kwargs)
120
+ def send_message_bg(self, *args, **kwargs):
121
+ task = asyncio.create_task(self.send_message(*args, **kwargs))
122
+ task.add_done_callback(lambda t: t.exception() and log.error(f"BG send failed: {t.exception()}"))
123
+ return task
124
+
125
+ async def change_presence(self, *args, **kwargs): return await self.user_actions.change_presence(*args, **kwargs)