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.
- aiko_api-0.1.1/PKG-INFO +78 -0
- aiko_api-0.1.1/README.md +60 -0
- aiko_api-0.1.1/aiko_api/__init__.py +11 -0
- aiko_api-0.1.1/aiko_api/actions/__init__.py +0 -0
- aiko_api-0.1.1/aiko_api/actions/guilds.py +61 -0
- aiko_api-0.1.1/aiko_api/actions/messages.py +60 -0
- aiko_api-0.1.1/aiko_api/actions/user.py +71 -0
- aiko_api-0.1.1/aiko_api/cache.py +136 -0
- aiko_api-0.1.1/aiko_api/client.py +125 -0
- aiko_api-0.1.1/aiko_api/commands.py +364 -0
- aiko_api-0.1.1/aiko_api/common/__init__.py +0 -0
- aiko_api-0.1.1/aiko_api/common/constants.py +25 -0
- aiko_api-0.1.1/aiko_api/common/errors.py +32 -0
- aiko_api-0.1.1/aiko_api/common/models.py +127 -0
- aiko_api-0.1.1/aiko_api/core/__init__.py +0 -0
- aiko_api-0.1.1/aiko_api/core/headers.py +27 -0
- aiko_api-0.1.1/aiko_api/core/http.py +195 -0
- aiko_api-0.1.1/aiko_api/gateway.py +275 -0
- aiko_api-0.1.1/aiko_api/http_curl.py +280 -0
- aiko_api-0.1.1/aiko_api/katlog.py +88 -0
- aiko_api-0.1.1/aiko_api/utils.py +48 -0
- aiko_api-0.1.1/aiko_api.egg-info/PKG-INFO +78 -0
- aiko_api-0.1.1/aiko_api.egg-info/SOURCES.txt +26 -0
- aiko_api-0.1.1/aiko_api.egg-info/dependency_links.txt +1 -0
- aiko_api-0.1.1/aiko_api.egg-info/requires.txt +6 -0
- aiko_api-0.1.1/aiko_api.egg-info/top_level.txt +1 -0
- aiko_api-0.1.1/pyproject.toml +30 -0
- aiko_api-0.1.1/setup.cfg +4 -0
aiko_api-0.1.1/PKG-INFO
ADDED
|
@@ -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.
|
aiko_api-0.1.1/README.md
ADDED
|
@@ -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)
|