mirror of
https://github.com/MelisaDev/melisa.git
synced 2024-09-22 19:22:01 +03:00
initial commit
This commit is contained in:
commit
d3472d6c82
20 changed files with 780 additions and 0 deletions
32
.gitignore
vendored
Normal file
32
.gitignore
vendored
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
# IDE Configuration files
|
||||||
|
.idea
|
||||||
|
.vscode
|
||||||
|
.vs
|
||||||
|
|
||||||
|
# Python Environment
|
||||||
|
*.py[dcw]
|
||||||
|
__pycache__
|
||||||
|
venv
|
||||||
|
virtualenv
|
||||||
|
pyvenv
|
||||||
|
Pipfile.lock
|
||||||
|
|
||||||
|
# Docs
|
||||||
|
docs/_build
|
||||||
|
|
||||||
|
# Dev Tools
|
||||||
|
tools/*
|
||||||
|
|
||||||
|
# Dev testing
|
||||||
|
dev
|
||||||
|
_testing.py
|
||||||
|
|
||||||
|
# Packaging
|
||||||
|
*.egg-info
|
||||||
|
bin
|
||||||
|
build
|
||||||
|
dist
|
||||||
|
MANIFEST
|
||||||
|
|
||||||
|
# IDE Ext's
|
||||||
|
.history/
|
14
README.md
Normal file
14
README.md
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
<p align="center">
|
||||||
|
<b>
|
||||||
|
The easiest way to create your own Discord Bot.
|
||||||
|
</b>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
## WARNING
|
||||||
|
**THIS LIBRARY IS CURRENTLY UNDER DEVELOPMENT!**
|
||||||
|
|
||||||
|
## About
|
||||||
|
<strong>MelisaPy</strong> is a Python library for the [Discord API](https://discord.com/developers/docs/intro).
|
||||||
|
|
||||||
|
We are trying to make our library optimized. We are going to create really big Cache configuration, so don't worry about the RAM :)
|
0
melisa/__init__.py
Normal file
0
melisa/__init__.py
Normal file
42
melisa/client.py
Normal file
42
melisa/client.py
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
from .models.app import Shard
|
||||||
|
from .utils.types import Coro
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
from typing import Dict
|
||||||
|
|
||||||
|
|
||||||
|
class Client:
|
||||||
|
def __init__(self, token, intents, **kwargs):
|
||||||
|
self.shards: Dict[int, Shard] = {}
|
||||||
|
self._events = {}
|
||||||
|
|
||||||
|
self.guilds = []
|
||||||
|
|
||||||
|
self.loop = asyncio.get_event_loop()
|
||||||
|
|
||||||
|
self.intents = intents
|
||||||
|
self._token = token
|
||||||
|
|
||||||
|
self._activity = kwargs.get("activity")
|
||||||
|
self._status = kwargs.get("status")
|
||||||
|
|
||||||
|
def listen(self, callback: Coro):
|
||||||
|
"""Method to set the listener.
|
||||||
|
Args:
|
||||||
|
callback (:obj:`function`)
|
||||||
|
Coroutine Callback Function
|
||||||
|
"""
|
||||||
|
if not asyncio.iscoroutinefunction(callback):
|
||||||
|
raise TypeError(f"<{callback.__qualname__}> must be a coroutine function")
|
||||||
|
|
||||||
|
self._events[callback.__qualname__] = callback
|
||||||
|
return self
|
||||||
|
|
||||||
|
def run(self) -> None:
|
||||||
|
"""
|
||||||
|
Run Bot without shards (only 0 shard)
|
||||||
|
"""
|
||||||
|
inited_shard = Shard(self, 0, 1)
|
||||||
|
|
||||||
|
asyncio.ensure_future(inited_shard.launch(activity=self._activity, status=self._status), loop=self.loop)
|
||||||
|
self.loop.run_forever()
|
1
melisa/core/__init__.py
Normal file
1
melisa/core/__init__.py
Normal file
|
@ -0,0 +1 @@
|
||||||
|
from .gateway import Gateway
|
177
melisa/core/gateway.py
Normal file
177
melisa/core/gateway.py
Normal file
|
@ -0,0 +1,177 @@
|
||||||
|
import json
|
||||||
|
import asyncio
|
||||||
|
import zlib
|
||||||
|
import time
|
||||||
|
|
||||||
|
import websockets
|
||||||
|
|
||||||
|
from ..listeners import listeners
|
||||||
|
from ..models.user import BotActivity
|
||||||
|
|
||||||
|
|
||||||
|
class Gateway:
|
||||||
|
|
||||||
|
DISPATCH = 0
|
||||||
|
HEARTBEAT = 1
|
||||||
|
IDENTIFY = 2
|
||||||
|
PRESENCE_UPDATE = 3
|
||||||
|
VOICE_UPDATE = 4
|
||||||
|
RESUME = 6
|
||||||
|
RECONNECT = 7
|
||||||
|
REQUEST_MEMBERS = 8
|
||||||
|
INVALID_SESSION = 9
|
||||||
|
HELLO = 10
|
||||||
|
HEARTBEAT_ACK = 11
|
||||||
|
|
||||||
|
def __init__(self,
|
||||||
|
client,
|
||||||
|
shard_id: int = 0,
|
||||||
|
num_shards: int = 1,
|
||||||
|
**kwargs):
|
||||||
|
|
||||||
|
self.GATEWAY_VERSION = "9"
|
||||||
|
self.interval = None
|
||||||
|
self.intents = client.intents
|
||||||
|
self.sequence = None
|
||||||
|
self.session_id = None
|
||||||
|
self.client = client
|
||||||
|
self.shard_id = shard_id
|
||||||
|
self.latency = float('inf')
|
||||||
|
|
||||||
|
self.listeners = listeners
|
||||||
|
|
||||||
|
self._last_send = 0
|
||||||
|
|
||||||
|
self.auth = {
|
||||||
|
"token": self.client._token,
|
||||||
|
"intents": self.intents,
|
||||||
|
"properties": {
|
||||||
|
"$os": "windows",
|
||||||
|
"$browser": "melisa",
|
||||||
|
"$device": "melisa"
|
||||||
|
},
|
||||||
|
"compress": True,
|
||||||
|
"shard": [shard_id, num_shards],
|
||||||
|
"presence": self.generate_presence(kwargs.get("start_activity"), kwargs.get("start_status"))
|
||||||
|
}
|
||||||
|
|
||||||
|
self._zlib: zlib._Decompress = zlib.decompressobj()
|
||||||
|
self._buffer: bytearray = bytearray()
|
||||||
|
|
||||||
|
async def websocket_message(self, msg):
|
||||||
|
if type(msg) is bytes:
|
||||||
|
self._buffer.extend(msg)
|
||||||
|
|
||||||
|
if len(msg) < 4 or msg[-4:] != b'\x00\x00\xff\xff':
|
||||||
|
return None
|
||||||
|
msg = self._zlib.decompress(self._buffer)
|
||||||
|
msg = msg.decode('utf-8')
|
||||||
|
self._buffer = bytearray()
|
||||||
|
|
||||||
|
return json.loads(msg)
|
||||||
|
|
||||||
|
async def start_loop(self):
|
||||||
|
async with websockets.connect(
|
||||||
|
f'wss://gateway.discord.gg/?v={self.GATEWAY_VERSION}&encoding=json&compress=zlib-stream') \
|
||||||
|
as self.websocket:
|
||||||
|
await self.hello()
|
||||||
|
if self.interval is None:
|
||||||
|
return
|
||||||
|
await asyncio.gather(self.heartbeat(), self.receive())
|
||||||
|
|
||||||
|
async def close(self, code: int = 4000):
|
||||||
|
await self.websocket.close(code=code)
|
||||||
|
|
||||||
|
async def resume(self):
|
||||||
|
resume_data = {
|
||||||
|
"seq": self.sequence,
|
||||||
|
"session_id": self.session_id,
|
||||||
|
"token": self.client._token
|
||||||
|
}
|
||||||
|
|
||||||
|
await self.send(self.RESUME, resume_data)
|
||||||
|
|
||||||
|
async def receive(self):
|
||||||
|
async for msg in self.websocket:
|
||||||
|
msg = await self.websocket_message(msg)
|
||||||
|
|
||||||
|
if msg is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
if msg["op"] == self.HEARTBEAT_ACK:
|
||||||
|
self.latency = time.time() - self._last_send
|
||||||
|
|
||||||
|
if msg["op"] == self.DISPATCH:
|
||||||
|
self.sequence = int(msg["s"])
|
||||||
|
event_type = msg["t"].lower()
|
||||||
|
|
||||||
|
event_to_call = self.listeners.get(event_type)
|
||||||
|
|
||||||
|
if event_to_call is not None:
|
||||||
|
await event_to_call(self.client, self, msg["d"])
|
||||||
|
|
||||||
|
if msg["op"] != self.DISPATCH:
|
||||||
|
if msg["op"] == self.RECONNECT:
|
||||||
|
await self.websocket.close()
|
||||||
|
await self.resume()
|
||||||
|
|
||||||
|
async def send(self, opcode, payload):
|
||||||
|
data = self.opcode(opcode, payload)
|
||||||
|
|
||||||
|
if opcode == self.HEARTBEAT:
|
||||||
|
self._last_send = time.time()
|
||||||
|
|
||||||
|
await self.websocket.send(data)
|
||||||
|
|
||||||
|
async def heartbeat(self):
|
||||||
|
while self.interval is not None:
|
||||||
|
await self.send(self.HEARTBEAT, self.sequence)
|
||||||
|
await asyncio.sleep(self.interval)
|
||||||
|
|
||||||
|
async def hello(self):
|
||||||
|
await self.send(self.IDENTIFY, self.auth)
|
||||||
|
|
||||||
|
ret = await self.websocket.recv()
|
||||||
|
|
||||||
|
data = await self.websocket_message(ret)
|
||||||
|
|
||||||
|
opcode = data["op"]
|
||||||
|
|
||||||
|
if opcode != 10:
|
||||||
|
return
|
||||||
|
|
||||||
|
self.interval = (data["d"]["heartbeat_interval"] - 2000) / 1000
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def generate_presence(activity: BotActivity = None, status: str = None):
|
||||||
|
data = {
|
||||||
|
"since": time.time() * 1000,
|
||||||
|
"afk": False
|
||||||
|
}
|
||||||
|
|
||||||
|
if activity is not None:
|
||||||
|
activity_to_set = {
|
||||||
|
"name": activity.name,
|
||||||
|
"type": int(activity.type)
|
||||||
|
}
|
||||||
|
|
||||||
|
if int(activity.type) == 1 and activity.url:
|
||||||
|
activity_to_set["url"] = activity.url
|
||||||
|
|
||||||
|
data["activities"] = [activity_to_set]
|
||||||
|
|
||||||
|
if status is not None:
|
||||||
|
data["status"] = str(status)
|
||||||
|
|
||||||
|
return data
|
||||||
|
|
||||||
|
async def update_presence(self, data: dict):
|
||||||
|
await self.send(self.PRESENCE_UPDATE, data)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def opcode(opcode: int, payload) -> str:
|
||||||
|
data = {
|
||||||
|
"op": opcode,
|
||||||
|
"d": payload
|
||||||
|
}
|
||||||
|
return json.dumps(data)
|
30
melisa/listeners/__init__.py
Normal file
30
melisa/listeners/__init__.py
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from glob import glob
|
||||||
|
from importlib import import_module
|
||||||
|
from os import chdir
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
def get_listeners():
|
||||||
|
listeners_list = {}
|
||||||
|
|
||||||
|
chdir(Path(__file__).parent.resolve())
|
||||||
|
|
||||||
|
for listener_path in glob("*.py"):
|
||||||
|
if listener_path.startswith("__"):
|
||||||
|
continue
|
||||||
|
|
||||||
|
event = listener_path[:-3]
|
||||||
|
|
||||||
|
try:
|
||||||
|
listeners_list[event] = getattr(
|
||||||
|
import_module(f".{event}", package=__name__), "export"
|
||||||
|
)()
|
||||||
|
except AttributeError:
|
||||||
|
continue
|
||||||
|
|
||||||
|
return listeners_list
|
||||||
|
|
||||||
|
|
||||||
|
listeners = get_listeners()
|
20
melisa/listeners/guild_create.py
Normal file
20
melisa/listeners/guild_create.py
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from ..utils.types import Coro
|
||||||
|
|
||||||
|
|
||||||
|
async def guild_create_listener(self, gateway, payload: dict):
|
||||||
|
gateway.session_id = payload.get("session_id")
|
||||||
|
|
||||||
|
self.guilds[payload["id"]] = payload
|
||||||
|
|
||||||
|
custom_listener = self._events.get("on_guild_create")
|
||||||
|
|
||||||
|
if custom_listener is not None:
|
||||||
|
await custom_listener(payload) # ToDo: Guild Model
|
||||||
|
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
def export() -> Coro:
|
||||||
|
return guild_create_listener
|
22
melisa/listeners/ready.py
Normal file
22
melisa/listeners/ready.py
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from ..utils.types import Coro
|
||||||
|
|
||||||
|
|
||||||
|
async def on_ready_listener(self, gateway, payload: dict):
|
||||||
|
gateway.session_id = payload.get("session_id")
|
||||||
|
|
||||||
|
guilds = payload.get("guilds")
|
||||||
|
|
||||||
|
self.guilds = dict(map(lambda i: (i["id"], None), guilds))
|
||||||
|
|
||||||
|
custom_listener = self._events.get("on_ready")
|
||||||
|
|
||||||
|
if custom_listener is not None:
|
||||||
|
await custom_listener()
|
||||||
|
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
def export() -> Coro:
|
||||||
|
return on_ready_listener
|
1
melisa/models/__init__.py
Normal file
1
melisa/models/__init__.py
Normal file
|
@ -0,0 +1 @@
|
||||||
|
from .app import Intents
|
2
melisa/models/app/__init__.py
Normal file
2
melisa/models/app/__init__.py
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
from .intents import *
|
||||||
|
from .shard import *
|
26
melisa/models/app/intents.py
Normal file
26
melisa/models/app/intents.py
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from enum import IntFlag
|
||||||
|
|
||||||
|
|
||||||
|
class Intents(IntFlag):
|
||||||
|
NONE = 0
|
||||||
|
GUILDS = 1 << 0
|
||||||
|
GUILD_MEMBERS = 1 << 1
|
||||||
|
GUILD_BANS = 1 << 2
|
||||||
|
GUILD_EMOJIS_AND_STICKERS = 1 << 3
|
||||||
|
GUILD_INTEGRATIONS = 1 << 4
|
||||||
|
GUILD_WEBHOOKS = 1 << 5
|
||||||
|
GUILD_INVITES = 1 << 6
|
||||||
|
GUILD_VOICE_STATES = 1 << 7
|
||||||
|
GUILD_PRESENCES = 1 << 8
|
||||||
|
GUILD_MESSAGES = 1 << 9
|
||||||
|
GUILD_MESSAGE_REACTIONS = 1 << 10
|
||||||
|
GUILD_MESSAGE_TYPING = 1 << 11
|
||||||
|
DIRECT_MESSAGES = 1 << 12
|
||||||
|
DIRECT_MESSAGE_REACTIONS = 1 << 13
|
||||||
|
DIRECT_MESSAGE_TYPING = 1 << 14
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def all(cls) -> Intents:
|
||||||
|
return cls(sum(cls))
|
64
melisa/models/app/shard.py
Normal file
64
melisa/models/app/shard.py
Normal file
|
@ -0,0 +1,64 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from asyncio import create_task
|
||||||
|
|
||||||
|
from ...core.gateway import Gateway
|
||||||
|
from ..user import BotActivity
|
||||||
|
|
||||||
|
|
||||||
|
class Shard:
|
||||||
|
def __init__(self,
|
||||||
|
client,
|
||||||
|
shard_id: int,
|
||||||
|
num_shards: int):
|
||||||
|
self._client = client
|
||||||
|
|
||||||
|
self._shard_id: int = shard_id
|
||||||
|
self._num_shards: num_shards = num_shards
|
||||||
|
self._gateway: Gateway
|
||||||
|
|
||||||
|
self.disconnected = None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def id(self) -> int:
|
||||||
|
"""Id of Shard"""
|
||||||
|
return self._gateway.shard_id
|
||||||
|
|
||||||
|
async def launch(self, **kwargs) -> Shard:
|
||||||
|
"""Launch new shard"""
|
||||||
|
self._gateway = Gateway(self._client,
|
||||||
|
self._shard_id,
|
||||||
|
self._num_shards,
|
||||||
|
start_activity=kwargs.get("activity"),
|
||||||
|
start_status=kwargs.get("status"))
|
||||||
|
|
||||||
|
self._client.shards[self._shard_id] = self
|
||||||
|
|
||||||
|
self.disconnected = False
|
||||||
|
|
||||||
|
create_task(self._gateway.start_loop())
|
||||||
|
|
||||||
|
return self
|
||||||
|
|
||||||
|
async def update_presence(self, activity: BotActivity = None, status: str = None) -> Shard:
|
||||||
|
"""
|
||||||
|
Update Presence for the shard
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
activity: :class:`Activity`
|
||||||
|
Activity to set
|
||||||
|
status: :class:`str`
|
||||||
|
Status to set (You can use :class:`~melisa.models.users.StatusType` to generate it)
|
||||||
|
"""
|
||||||
|
data = self._gateway.generate_presence(activity, status)
|
||||||
|
await self._gateway.update_presence(data)
|
||||||
|
|
||||||
|
return self
|
||||||
|
|
||||||
|
async def disconnect(self) -> Shard:
|
||||||
|
await self._gateway.close()
|
||||||
|
|
||||||
|
self.disconnected = True
|
||||||
|
|
||||||
|
return self
|
1
melisa/models/guild/__init__.py
Normal file
1
melisa/models/guild/__init__.py
Normal file
|
@ -0,0 +1 @@
|
||||||
|
from .guild import *
|
6
melisa/models/guild/guild.py
Normal file
6
melisa/models/guild/guild.py
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
from ...utils import Snowflake
|
||||||
|
|
||||||
|
class Guild:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
1
melisa/models/user/__init__.py
Normal file
1
melisa/models/user/__init__.py
Normal file
|
@ -0,0 +1 @@
|
||||||
|
from .presence import *
|
250
melisa/models/user/presence.py
Normal file
250
melisa/models/user/presence.py
Normal file
|
@ -0,0 +1,250 @@
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from enum import IntEnum, Enum, Flag
|
||||||
|
from typing import Optional, Tuple, List, Literal
|
||||||
|
|
||||||
|
from ...utils import Snowflake
|
||||||
|
|
||||||
|
|
||||||
|
class BasePresenceData:
|
||||||
|
"""
|
||||||
|
All the information about activities here is from the Discord API docs.
|
||||||
|
Read more here: https://discord.com/developers/docs/topics/gateway#activity-object
|
||||||
|
|
||||||
|
Unknown data will be returned as None.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class ActivityType(IntEnum):
|
||||||
|
"""Represents the enum of the type of activity.
|
||||||
|
|
||||||
|
Attributes
|
||||||
|
----------
|
||||||
|
GAME:
|
||||||
|
Playing {name} (Playing Rocket League)
|
||||||
|
STREAMING:
|
||||||
|
Streaming {details} (Streaming Rocket League) It supports only YouTube and Twitch
|
||||||
|
LISTENING:
|
||||||
|
Listening to {name} (Listening to Spotify)
|
||||||
|
WATCHING:
|
||||||
|
Watching {name} (Watching YouTube Together)
|
||||||
|
CUSTOM:
|
||||||
|
{emoji} {name} (":smiley: I am cool") (THIS ACTIVITY IS NOT SUPPORTED FOR BOTS)
|
||||||
|
COMPETING:
|
||||||
|
Competing in {name} (Competing in Arena World Champions)
|
||||||
|
"""
|
||||||
|
GAME = 0
|
||||||
|
STREAMING = 1
|
||||||
|
LISTENING = 2
|
||||||
|
WATCHING = 3
|
||||||
|
CUSTOM = 4
|
||||||
|
COMPETING = 5
|
||||||
|
|
||||||
|
def __int__(self):
|
||||||
|
return self.value
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(repr=False)
|
||||||
|
class ActivityTimestamp(BasePresenceData):
|
||||||
|
"""Represents the timestamp of an activity.
|
||||||
|
|
||||||
|
Attributes
|
||||||
|
----------
|
||||||
|
start: Optional[:class:`int`]
|
||||||
|
Unix time (in milliseconds) of when the activity started
|
||||||
|
end: Optional[:class:`int`]
|
||||||
|
Unix time (in milliseconds) of when the activity ends
|
||||||
|
"""
|
||||||
|
start: Optional[int] = None
|
||||||
|
end: Optional[int] = None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(repr=False)
|
||||||
|
class ActivityEmoji(BasePresenceData):
|
||||||
|
"""Represents an emoji in an activity.
|
||||||
|
|
||||||
|
Attributes
|
||||||
|
----------
|
||||||
|
name: :class:`str`
|
||||||
|
The name of the emoji
|
||||||
|
id: Optional[:class:`Snowflake`]
|
||||||
|
The id of the emoji
|
||||||
|
animated: Optional[:class:`bool`]
|
||||||
|
Whether this emoji is animated
|
||||||
|
"""
|
||||||
|
name: str
|
||||||
|
id: Optional[Snowflake] = None
|
||||||
|
animated: Optional[bool] = None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(repr=False)
|
||||||
|
class ActivityParty(BasePresenceData):
|
||||||
|
"""Represents a party in an activity.
|
||||||
|
|
||||||
|
Attributes
|
||||||
|
----------
|
||||||
|
id: Optional[:class:`str`]
|
||||||
|
The id of the party
|
||||||
|
size: Optional[Tuple[:class:`int`, :class:`int`]]
|
||||||
|
Array of two integers (current_size, max_size)
|
||||||
|
"""
|
||||||
|
id: Optional[str] = None
|
||||||
|
size: Optional[Tuple[int, int]] = None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(repr=False)
|
||||||
|
class ActivityAssets(BasePresenceData):
|
||||||
|
"""Represents an asset of an activity.
|
||||||
|
|
||||||
|
Attributes
|
||||||
|
----------
|
||||||
|
large_image: Optional[:class:`str`]
|
||||||
|
(Large Image) Activity asset images are arbitrary strings which usually contain snowflake IDs
|
||||||
|
large_text: Optional[:class:`str`]
|
||||||
|
text displayed when hovering over the large image of the activity
|
||||||
|
small_image: Optional[:class:`str`]
|
||||||
|
(Small Image) Activity asset images are arbitrary strings which usually contain snowflake IDs
|
||||||
|
small_text: Optional[:class:`str`]
|
||||||
|
text displayed when hovering over the small image of the activity
|
||||||
|
"""
|
||||||
|
large_image: Optional[str] = None
|
||||||
|
large_text: Optional[str] = None
|
||||||
|
small_image: Optional[str] = None
|
||||||
|
small_text: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(repr=False)
|
||||||
|
class ActivitySecrets(BasePresenceData):
|
||||||
|
"""Represents a secret of an activity.
|
||||||
|
|
||||||
|
Attributes
|
||||||
|
----------
|
||||||
|
join: Optional[:class:`str`]
|
||||||
|
The secret for joining a party
|
||||||
|
spectate: Optional[:class:`str`]
|
||||||
|
The secret for spectating a game
|
||||||
|
match: Optional[:class:`str`]
|
||||||
|
The secret for a specific instanced match
|
||||||
|
"""
|
||||||
|
join: Optional[str] = None
|
||||||
|
spectate: Optional[str] = None
|
||||||
|
match_: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
class ActivityFlags(BasePresenceData):
|
||||||
|
"""
|
||||||
|
Just Activity Flags (From Discord API).
|
||||||
|
|
||||||
|
Everything returns :class:`bool` value.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, flags) -> None:
|
||||||
|
self.INSTANCE = bool(flags >> 0 & 1)
|
||||||
|
self.JOIN = bool(flags >> 1 & 1)
|
||||||
|
self.SPECTATE = bool(flags >> 2 & 1)
|
||||||
|
self.JOIN_REQUEST = bool(flags >> 3 & 1)
|
||||||
|
self.SYNC = bool(flags >> 4 & 1)
|
||||||
|
self.PLAY = bool(flags >> 5 & 1)
|
||||||
|
self.PARTY_PRIVACY_FRIENDS = bool(flags >> 6 & 1)
|
||||||
|
self.PARTY_PRIVACY_VOICE_CHANNEL = bool(flags >> 7 & 1)
|
||||||
|
self.EMBEDDED = bool(flags >> 8 & 1)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(repr=False)
|
||||||
|
class ActivityButton(BasePresenceData):
|
||||||
|
"""When received over the gateway, the buttons field is an array of strings, which are the button labels. Bots
|
||||||
|
cannot access a user's activity button URLs. When sending, the buttons field must be an array of the below
|
||||||
|
object:
|
||||||
|
Attributes
|
||||||
|
----------
|
||||||
|
label: :class:`str`
|
||||||
|
The text shown on the button (1-32 characters)
|
||||||
|
url: :class:`str`
|
||||||
|
The url opened when clicking the button (1-512 characters)
|
||||||
|
"""
|
||||||
|
label: str
|
||||||
|
url: str
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(repr=False)
|
||||||
|
class Activity(BasePresenceData):
|
||||||
|
"""Bots are only able to send ``name``, ``type``, and optionally ``url``.
|
||||||
|
|
||||||
|
Attributes
|
||||||
|
----------
|
||||||
|
name: :class:`str`
|
||||||
|
The activity's name
|
||||||
|
type: :class:`~melisa.models.user.activity.ActivityType`
|
||||||
|
Activity type
|
||||||
|
url: Optional[:class:`str`]
|
||||||
|
Stream url, is validated when type is 1
|
||||||
|
created_at: :class:`int`
|
||||||
|
Unix timestamp (in milliseconds) of when the activity was added to the user's session
|
||||||
|
timestamps: Optional[:class:`~melisa.models.user.activity.ActivityTimestamp`]
|
||||||
|
Unix timestamps for start and/or end of the game
|
||||||
|
application_id: Optional[:class:`~melisa.utils.snowflake.Snowflake`]
|
||||||
|
Application id for the game
|
||||||
|
details: Optional[:class:`str`]
|
||||||
|
What the player is currently doing
|
||||||
|
state: Optional[:class:`str`]
|
||||||
|
The user's current party status
|
||||||
|
emoji: Optional[:class:`~melisa.models.user.activity.ActivityEmoji`]
|
||||||
|
The emoji used for a custom status
|
||||||
|
party: Optional[:class:`~melisa.models.user.activity.ActivityParty`]
|
||||||
|
Information for the current party of the player
|
||||||
|
assets: Optional[:class:`~melisa.models.user.activity.ActivityAssets`]
|
||||||
|
Images for the presence and their hover texts
|
||||||
|
secrets: Optional[:class:`~melisa.models.user.activity.ActivitySecrets`]
|
||||||
|
Secrets for Rich Presence joining and spectating
|
||||||
|
instance: Optional[:class:`bool`]
|
||||||
|
Whether or not the activity is an instanced game session
|
||||||
|
flags: Optional[:class:`~melisa.models.user.activity.ActivityFlags`]
|
||||||
|
Activity flags ORd together, describes what the payload includes
|
||||||
|
buttons: Optional[List[:class:`~melisa.models.user.activity.ActivityButton`]]
|
||||||
|
The url button on an activity.
|
||||||
|
"""
|
||||||
|
|
||||||
|
name: str
|
||||||
|
type: ActivityType
|
||||||
|
created_at: int
|
||||||
|
|
||||||
|
url: Optional[str] = None
|
||||||
|
timestamps: Optional[ActivityTimestamp] = None
|
||||||
|
application_id: Optional[Snowflake] = None
|
||||||
|
details: Optional[str] = None
|
||||||
|
state: Optional[str] = None
|
||||||
|
emoji: Optional[ActivityEmoji] = None
|
||||||
|
party: Optional[ActivityParty] = None
|
||||||
|
assets: Optional[ActivityAssets] = None
|
||||||
|
secrets: Optional[ActivitySecrets] = None
|
||||||
|
instance: Optional[bool] = None
|
||||||
|
flags: Optional[ActivityFlags] = None
|
||||||
|
buttons: Optional[List[ActivityButton]] = None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(repr=False)
|
||||||
|
class BotActivity(BasePresenceData):
|
||||||
|
"""
|
||||||
|
|
||||||
|
Attributes
|
||||||
|
----------
|
||||||
|
name: :class:`str`
|
||||||
|
The activity's name
|
||||||
|
type: :class:`~melisa.models.user.activity.ActivityType`
|
||||||
|
Activity type
|
||||||
|
url: Optional[:class:`str`]
|
||||||
|
Stream url, is validated when type is Streaming"""
|
||||||
|
|
||||||
|
name: str
|
||||||
|
type: ActivityType
|
||||||
|
url: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
class StatusType(Enum):
|
||||||
|
ONLINE = 'online'
|
||||||
|
OFFLINE = 'offline'
|
||||||
|
IDLE = 'idle'
|
||||||
|
DND = 'dnd'
|
||||||
|
INVISIBLE = 'invisible'
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.value
|
10
melisa/utils/__init__.py
Normal file
10
melisa/utils/__init__.py
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
from .types import (
|
||||||
|
Coro
|
||||||
|
)
|
||||||
|
|
||||||
|
from .snowflake import Snowflake
|
||||||
|
|
||||||
|
__all__ = (
|
||||||
|
"Coro",
|
||||||
|
"Snowflake"
|
||||||
|
)
|
69
melisa/utils/snowflake.py
Normal file
69
melisa/utils/snowflake.py
Normal file
|
@ -0,0 +1,69 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
|
||||||
|
class Snowflake(int):
|
||||||
|
"""
|
||||||
|
Discord utilizes Twitter's snowflake format for uniquely identifiable descriptors (IDs). These IDs are guaranteed
|
||||||
|
to be unique across all of Discord, except in some unique scenarios in which child objects share their parent's
|
||||||
|
ID. Because Snowflake IDs are up to 64 bits in size (e.g. a uint64), they are always returned as strings in the
|
||||||
|
HTTP API to prevent integer overflows in some languages. See Gateway ETF/JSON for more information regarding
|
||||||
|
Gateway encoding.
|
||||||
|
|
||||||
|
Read more here: https://discord.com/developers/docs/reference#snowflakes
|
||||||
|
"""
|
||||||
|
|
||||||
|
_MAX_VALUE: int = 9223372036854775807
|
||||||
|
_MIN_VALUE: int = 0
|
||||||
|
|
||||||
|
def __init__(self, _):
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
|
if self < self._MIN_VALUE:
|
||||||
|
raise ValueError(
|
||||||
|
"snowflake value should be greater than or equal to 0."
|
||||||
|
)
|
||||||
|
|
||||||
|
if self > self._MAX_VALUE:
|
||||||
|
raise ValueError(
|
||||||
|
"snowflake value should be less than or equal to 9223372036854775807."
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def __factory__(cls, string: str) -> Snowflake:
|
||||||
|
return cls.from_string(string)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_string(cls, string: str):
|
||||||
|
"""Initialize a new Snowflake from a string.
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
string: :class:`str`
|
||||||
|
The snowflake as a string.
|
||||||
|
"""
|
||||||
|
return Snowflake(int(string))
|
||||||
|
|
||||||
|
@property
|
||||||
|
def timestamp(self) -> int:
|
||||||
|
"""
|
||||||
|
Milliseconds since Discord Epoch, the first second of 2015 or 1420070400000.
|
||||||
|
"""
|
||||||
|
return self >> 22
|
||||||
|
|
||||||
|
@property
|
||||||
|
def worker_id(self) -> int:
|
||||||
|
"""Internal worker ID"""
|
||||||
|
return (self >> 17) % 16
|
||||||
|
|
||||||
|
@property
|
||||||
|
def process_id(self) -> int:
|
||||||
|
"""Internal process ID"""
|
||||||
|
return (self >> 12) % 16
|
||||||
|
|
||||||
|
@property
|
||||||
|
def increment(self) -> int:
|
||||||
|
""" For every ID that is generated on that process, this number is incremented"""
|
||||||
|
return self % 2048
|
||||||
|
|
||||||
|
@property
|
||||||
|
def unix(self) -> int:
|
||||||
|
return self.timestamp + 1420070400000
|
12
melisa/utils/types.py
Normal file
12
melisa/utils/types.py
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import TypeVar, Callable, Coroutine, Any, Union
|
||||||
|
|
||||||
|
T = TypeVar("T")
|
||||||
|
|
||||||
|
Coro = TypeVar("Coro", bound=Callable[..., Coroutine[Any, Any, Any]])
|
||||||
|
|
||||||
|
APINullable = Union[T, None]
|
||||||
|
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue