add most basic types

This commit is contained in:
Zomatree
2021-08-28 23:09:23 +01:00
parent 9b1f9276fa
commit 5e1bf92970
19 changed files with 447 additions and 44 deletions
+2
View File
@@ -1,3 +1,5 @@
.venv
**/__pycache__
test.py
dist
*.egg-info
+1
View File
@@ -2,3 +2,4 @@ from .channel import TextChannel, VoiceChannel
from .client import Client
from .server import Server
from .user import User
from .message import Message
+31
View File
@@ -0,0 +1,31 @@
from __future__ import annotations
from typing import TYPE_CHECKING, Union
if TYPE_CHECKING:
from .payloads import File as FilePayload
from .state import State
from io import IOBase
class Asset:
def __init__(self, data: FilePayload, state: State):
self.id = data['_id']
self.tag = data['tag']
self.size = data['size']
self.filename = data['filename']
metadata = data['metadata']
if metadata["type"] == "Image" or metadata["type"] == "Video": # cant use `in` because type narrowing wont happen
self.height = metadata["height"]
self.width = metadata["width"]
else:
self.height = None
self.width = None
self.content_type = data["content_type"]
async def read(self) -> bytes:
...
async def save(self, fp: IOBase):
fp.write(await self.read())
+10 -6
View File
@@ -1,6 +1,8 @@
from __future__ import annotations
from typing import TYPE_CHECKING, Text, Union
from .messageable import Messageable
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from .payloads import Channel as ChannelPayload
@@ -11,30 +13,32 @@ class Channel:
self.state = state
self.id = data["_id"]
self.channel_type = data["channel_type"]
self.server = None
class SavedMessageChannel(Channel):
class SavedMessageChannel(Channel, Messageable):
def __init__(self, data: ChannelPayload, state: State):
super().__init__(data, state)
class DMChannel(Channel):
class DMChannel(Channel, Messageable):
def __init__(self, data: ChannelPayload, state: State):
super().__init__(data, state)
class GroupDMChannel(Channel):
class GroupDMChannel(Channel, Messageable):
def __init__(self, data: ChannelPayload, state: State):
super().__init__(data, state)
class TextChannel(Channel):
class TextChannel(Channel, Messageable):
def __init__(self, data: ChannelPayload, state: State):
super().__init__(data, state)
Messageable.__init__(self, state)
class VoiceChannel(Channel):
def __init__(self, data: ChannelPayload, state: State):
super().__init__(data, state)
def channel_factory(data: ChannelPayload) -> type[Channel]:
# Literal["SavedMessage", "DirectMessage", "Group", "TextChannel", "VoiceChannel"]
channel_type = data["channel_type"]
if channel_type == "SavedMessage":
return SavedMessageChannel
elif channel_type == "DirectMessage":
+13 -8
View File
@@ -26,11 +26,11 @@ R = TypeVar("R")
Coro = Coroutine[Any, Any, R]
class Client:
def __init__(self, session: aiohttp.ClientSession, token: str, api_url: str = "https://api.revolt.chat"):
def __init__(self, session: aiohttp.ClientSession, token: str, api_url: str = "https://api.revolt.chat", max_messages: int = 5000):
self.session = session
self.token = token
self.api_url = api_url
self.events: dict[str, list[Callable[..., Coro[None]]]] = {}
self.max_messages = max_messages
self.api_info: ApiInfo
self.http: HttpClient
@@ -39,22 +39,27 @@ class Client:
def event(self, name: str = None):
def inner(func: Callable[..., Coro[None]]):
event_name = name or func.__name__
self.events.setdefault(event_name, []).append(func)
event_name = "on_{name}" if name else func.__name__
setattr(self, event_name, func)
return func
return inner
def dispatch(self, event: str, *args: Any):
...
func = getattr(self, "on_{event}", None)
if func:
asyncio.create_task(func(*args))
async def get_api_info(self) -> ApiInfo:
async with self.session.get(self.api_url) as resp:
return json.loads(await resp.text())
async def start(self):
async with self.session.get(self.api_url) as resp:
api_info: ApiInfo = json.loads(await resp.text())
api_info = await self.get_api_info()
self.api_info = api_info
self.http = HttpClient(self.session, self.token, self.api_url, self.api_info)
self.state = State(self.http, api_info)
self.state = State(self.http, api_info, self.max_messages)
self.websocket = WebsocketHandler(self.session, self.token, api_info["ws"], self.dispatch, self.state)
await self.websocket.start()
+14
View File
@@ -0,0 +1,14 @@
from __future__ import annotations
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from .payloads import Embed as EmbedPayload
class Embed:
@classmethod
def from_dict(cls, data: EmbedPayload):
...
def to_dict(self) -> EmbedPayload:
...
+8
View File
@@ -0,0 +1,8 @@
class RevoltError(Exception):
"Base exception for revolt"
class HTTPError(RevoltError):
"Base exception for http errors"
class ServerError(RevoltError):
"Internal server error"
+24
View File
@@ -0,0 +1,24 @@
from typing import Optional, Union
import io
import os
class File:
def __init__(self, file: Union[str, bytes], *, filename: Optional[str] = None, spoiler: bool = False):
if isinstance(file, str):
self.f = open(file, "rb")
elif isinstance(file, bytes):
self.f = io.BytesIO(file)
if filename is None and isinstance(file, str):
filename = self.f.name
if spoiler or (filename and filename.startswith("SPOILER_")):
self.spoiler = True
else:
self.spoiler = False
if self.spoiler and (filename and not filename.startswith("SPOILER_")):
filename = f"SPOILER_{filename}"
self.filename = filename
+80 -2
View File
@@ -1,14 +1,92 @@
from __future__ import annotations
from typing import TYPE_CHECKING
from typing import Any, Optional, TYPE_CHECKING, Literal
import aiohttp
from .errors import HTTPError, ServerError
from .file import File
try:
import ujson as _json
except ImportError:
import json as _json
if TYPE_CHECKING:
import aiohttp
from .payloads import ApiInfo
from .payloads import ApiInfo, Autumn as AutumnPayload, Message as MessagePayload, Embed as EmbedPayload
from .file import File
class HttpClient:
def __init__(self, session: aiohttp.ClientSession, token: str, api_url: str, api_info: ApiInfo):
self.session = session
self.token = token
self.api_url = api_url
self.api_info = api_info
async def request(self, method: Literal["GET", "POST", "PUT", "DELETE", "PATCH"], route: str, *, files: Optional[list[File]] = None, json: Optional[dict] = None) -> Any:
url = f"{self.api_url}{route}"
kwargs = {}
headers = {
"User-Agent": "Revolt.py https://github.com/Zomatree/revolt.py",
"x-bot-token": self.token
}
if json:
headers["Content-Type"] = "application/json"
kwargs["data"] = _json.dumps(json)
kwargs["headers"] = headers
async with self.session.request(method, url, **kwargs) as resp:
response = _json.loads(await resp.text())
resp_code = resp.status
if 200 <= resp_code <= 300:
return response
elif resp_code == 400:
raise HTTPError
async def upload_file(self, file: File, tag: str) -> AutumnPayload:
url = f"{self.api_info['features']['autumn']['url']}/{tag}"
headers = {
"User-Agent": "Revolt.py https://github.com/Zomatree/revolt.py"
}
form = aiohttp.FormData()
form.add_field("file", file.f.read(), filename=file.filename)
async with self.session.post(url, data=form, headers=headers) as resp:
response: AutumnPayload = _json.loads(await resp.text())
resp_code = resp.status
if resp_code == 400:
raise HTTPError(response)
elif 500 <= resp_code <= 600:
raise ServerError
else:
return response
async def send_message(self, channel: str, content: Optional[str], embeds: Optional[list[EmbedPayload]], attachments: Optional[list[File]]) -> MessagePayload:
json = {}
if content:
json["content"] = content
if embeds:
json["embeds"] = embeds
if attachments:
attachment_ids = []
for attachment in attachments:
data = await self.upload_file(attachment, "attachments")
attachment_ids.append(data["id"])
json["attachments"] = attachment_ids
return await self.request("POST", "/channels/{channel}/messages", json=json)
+17
View File
@@ -0,0 +1,17 @@
from __future__ import annotations
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from .state import State
from .payloads import Member as MemberPayload
from .server import Server
class Member:
def __init__(self, data: MemberPayload, server: Server, state: State):
self._state = state
self.id = data["_id"]
self.nickname = data.get("nickname")
self.roles = [server.get_role(role_id) for role_id in data.get("roles", [])]
self.server = server
+29
View File
@@ -0,0 +1,29 @@
from __future__ import annotations
from typing import TYPE_CHECKING
from .asset import Asset
from .embed import Embed
from .channel import TextChannel
if TYPE_CHECKING:
from .state import State
from .payloads import Message as MessagePayload
class Message:
def __init__(self, data: MessagePayload, state: State):
self.state = state
self.id = data["_id"]
self.content = data['content']
self.attachments = [Asset(attachment, state) for attachment in data.get('attachments', [])]
self.embeds = [Embed.from_dict(embed) for embed in data["embeds"]]
self.channel = state.get_channel(data['channel'])
self.server = self.channel and self.channel.server
if isinstance(self.channel, TextChannel):
self.author = state.get_member(self.id, data['author'])
else:
self.author = state.get_user(data['author'])
+25
View File
@@ -0,0 +1,25 @@
from __future__ import annotations
from typing import Optional, TYPE_CHECKING
if TYPE_CHECKING:
from .state import State
from .message import Message
from .embed import Embed
from .file import File
class Messageable:
id: str
def __init__(self, state: State):
self.state = state
async def send(self, content: Optional[str] = None, embeds: Optional[list[Embed]] = None, embed: Optional[Embed] = None, attachments: Optional[list[File]] = None) -> Message:
if embed:
embeds = [embed]
embed_payload = [embed.to_dict() for embed in embeds] if embeds else None
message = await self.state.http.send_message(self.id, content, embed_payload, attachments)
return self.state.add_message(message)
+101 -16
View File
@@ -1,8 +1,23 @@
from typing import Literal, TypedDict, Union
from typing import Literal, Type, TypedDict, Union
class ApiFeature(TypedDict):
enabled: bool
url: str
class VosoFeature(ApiFeature):
ws: str
class Features(TypedDict):
email: bool
invite_only: bool
captcha: ApiFeature
autumn: ApiFeature
january: ApiFeature
voso: VosoFeature
class ApiInfo(TypedDict):
revolt: str
features: dict
features: Features
ws: str
app: str
vapid: str
@@ -16,12 +31,15 @@ class AuthenticatePayload(BasePayload):
class UserBot(TypedDict):
owner: str
class _FileMetadataOptional(TypedDict, total=False):
width: int
class SizedMetadata(TypedDict):
type: Literal["Image", "Video"]
height: int
width: int
class FileMetadata(_FileMetadataOptional):
type: Literal["File", "Text", "Audio", "Image", "Video"]
class SimpleMetadata(TypedDict):
type: Literal["File", "Text", "Audio"]
FileMetadata = Union[SizedMetadata, SimpleMetadata]
class File(TypedDict):
_id: str
@@ -31,11 +49,15 @@ class File(TypedDict):
metadata: FileMetadata
content_type: str
class Status(TypedDict, total=False):
text: str
presence: Literal["Busy", "Idle", "Invisible", "Online"]
class _OptionalUser(TypedDict, total=False):
avatar: File
relations: list
badges: int
status: ...
status: Status
relationship: Literal["Blocked", "BlockedOther", "Friend", "Incoming", "None", "Outgoing", "User"]
online: bool
flags: int
@@ -82,31 +104,94 @@ class ChannelIconChanged(TypedDict):
class _OptionalMessage(TypedDict):
attachments: list[File]
class Embed(TypedDict):
pass # TODO
class Message(_OptionalMessage):
_id: str
channel: str
author: str
content: Union[str, UserAddContent, UserRemoveContent, UserJoinedContent, UserLeftContent, UserKickedContent, UserBanned, ChannelRenameContent, ChannelDescriptionChangeContent, ChannelIconChanged]
embeds: list[Embed]
class _OptionalChannel(TypedDict, total=False):
user: str
class _NonceChannel(TypedDict, total=False):
nonce: str
active: str
class BaseChannel(TypedDict):
_id: str
class SavedMessages(_NonceChannel, BaseChannel):
user: str
channel_type: Literal["SavedMessage"]
class DirectMessage(_NonceChannel, BaseChannel):
active: bool
recipients: list[str]
last_message: Message
channel_type: Literal["DirectMessage"]
class _GroupOptional(TypedDict):
icon: File
permissions: int
description: str
class Group(_NonceChannel, _GroupOptional, BaseChannel):
recipients: list[str]
name: str
owner: str
description: str
channel_type: Literal["Group"]
class _TextChannelOptional(TypedDict, total=False):
icon: File
default_permissions: int
role_permissions: int
class Channel(_OptionalChannel):
_id: str
channel_type: Literal["SavedMessage", "DirectMessage", "Group", "TextChannel", "VoiceChannel"]
class TextChannel(_NonceChannel, _TextChannelOptional, BaseChannel):
server: str
name: str
description: str
last_message: Message
channel_type: Literal["TextChannel"]
class ReadyPayload(BasePayload):
class _VoiceChannelOptional(TypedDict, total=False):
icon: File
default_permissions: int
role_permissions: int
class VoiceChannel(_NonceChannel, _TextChannelOptional, BaseChannel):
server: str
name: str
description: str
channel_type: Literal["VoiceChannel"]
Channel = Union[SavedMessages, DirectMessage, Group, TextChannel, VoiceChannel]
class ReadyEventPayload(BasePayload):
users: list[User]
servers: list[Server]
channels: list[Channel]
class MessageEventPayload(BasePayload):
class MessageEventPayload(BasePayload, Message):
pass
class Autumn(TypedDict):
id: str
class _MemberOptional(TypedDict, total=False):
nickname: str
avatar: File
roles: list[str]
class Member(_MemberOptional):
_id: str
Permission = tuple[int, int]
class _RoleOptional(TypedDict, total=False):
colour: str
hoist: bool
rank: int
class Role(_RoleOptional):
name: str
permissions: Permission
+8
View File
@@ -0,0 +1,8 @@
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from .payloads import Permission as PermissionTuple
class Permissions:
def __init__(self, permission_tuple: PermissionTuple):
...
+14
View File
@@ -0,0 +1,14 @@
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from .state import State
from .payloads import Role as RolePayload
class Role:
def __init__(self, data: RolePayload, role_id: str, state: State):
self.id = role_id
self.name = data["name"]
self.colour = data.get("colour")
self.hoist = data.get("hoist", False)
self.rank = data.get("rank", 0)
+14 -2
View File
@@ -1,13 +1,25 @@
from __future__ import annotations
from typing import TYPE_CHECKING
from typing import TYPE_CHECKING, Optional
if TYPE_CHECKING:
from .payloads import Server as ServerPayload
from .state import State
from .member import Member
from .role import Role
from .channel import Channel
class Server:
def __init__(self, data: ServerPayload, state: State):
self.state = state
self.id = data["_id"]
self._members: dict[str, Member] = {}
self._roles: dict[str, Role] = {}
self._channels: dict[str, Channel] = {}
def get_role(self, role_id: str, /) -> Optional[Role]:
return self._roles.get(role_id)
def get_member(self, member_id: str, /) -> Optional[Member]:
return self._members.get(member_id)
+36 -5
View File
@@ -1,38 +1,61 @@
from __future__ import annotations
from typing import TYPE_CHECKING, Optional
from collections import deque
from .user import User
from .channel import Channel, channel_factory
from .server import Server
from .message import Message
from .member import Member
if TYPE_CHECKING:
from .http import HttpClient
from .payloads import ApiInfo, User as UserPayload, Channel as ChannelPayload, Server as ServerPayload
from .payloads import ApiInfo, User as UserPayload, Channel as ChannelPayload, Server as ServerPayload, Message as MessagePayload, Member as MemberPayload
class State:
def __init__(self, http: HttpClient, api_info: ApiInfo):
def __init__(self, http: HttpClient, api_info: ApiInfo, max_messages: int):
self.http = http
self.api_info = api_info
self.max_messages = max_messages
self.users: dict[str, User] = {}
self.channels: dict[str, Channel] = {}
self.servers: dict[str, Server] = {}
self.messages: deque[Message] = deque()
def get_user(self, id: str) -> Optional[User]:
self.users.get(id)
return self.users.get(id)
def get_member(self, server_id: str, member_id: str) -> Optional[Member]:
server = self.servers.get(server_id)
if not server:
return
return server.get_member(member_id)
def get_channel(self, id: str) -> Optional[Channel]:
self.channels.get(id)
return self.channels.get(id)
def get_server(self, id: str) -> Optional[Server]:
self.channels.get(id)
return self.servers.get(id)
def add_user(self, payload: UserPayload) -> User:
user = User(payload, self)
self.users[user.id] = user
return user
def add_member(self, server_id: str, payload: MemberPayload) -> Optional[Member]:
server = self.get_server(server_id)
if not server:
return
member = Member(payload, server, self)
server._members[member.id] = member
return member
def add_channel(self, payload: ChannelPayload) -> Channel:
cls = channel_factory(payload)
channel = cls(payload, self)
@@ -43,3 +66,11 @@ class State:
server = Server(payload, self)
self.servers[server.id] = server
return server
def add_message(self, payload: MessagePayload) -> Message:
message = Message(payload, self)
if len(self.messages) >= self.max_messages:
self.messages.pop()
self.messages.appendleft(message)
return message
+5
View File
@@ -0,0 +1,5 @@
class _Missing:
def __repr__(self):
return "<Missing>"
Missing = _Missing()
+15 -5
View File
@@ -1,6 +1,7 @@
from __future__ import annotations
from revolt.payloads import MessageEventPayload
from typing import Callable, TYPE_CHECKING
from typing import Callable, TYPE_CHECKING, Coroutine, cast
import logging
import asyncio
@@ -11,13 +12,13 @@ except ImportError:
if TYPE_CHECKING:
import aiohttp
from .payloads import BasePayload, AuthenticatePayload, ReadyPayload
from .payloads import BasePayload, AuthenticatePayload, ReadyEventPayload, Message as MessagePayload
from .state import State
logger = logging.getLogger("revolt")
class WebsocketHandler:
def __init__(self, session: aiohttp.ClientSession, token: str, ws_url: str, dispatch: Callable, state: State):
def __init__(self, session: aiohttp.ClientSession, token: str, ws_url: str, dispatch: Callable[..., None], state: State):
self.session = session
self.token = token
self.ws_url = ws_url
@@ -52,16 +53,25 @@ class WebsocketHandler:
await func(payload)
async def handle_ready(self, payload: ReadyPayload):
async def handle_authenticated(self, _):
logger.info("Successfully authenticated")
async def handle_ready(self, payload: ReadyEventPayload):
for user in payload["users"]:
self.state.add_user(user)
for server in payload["servers"]:
self.state.add_server(server)
for channel in payload["channels"]:
self.state.add_channel(channel)
self.dispatch("ready")
async def handle_message(self, payload: MessageEventPayload):
message = self.state.add_message(cast(MessagePayload, payload))
self.dispatch("message", message)
async def start(self):
self.websocket = await self.session.ws_connect(self.ws_url)