From 013885dfb4e7e1077f037cc9205e75b215e09b7e Mon Sep 17 00:00:00 2001 From: Zomatree Date: Mon, 30 Aug 2021 16:22:16 +0100 Subject: [PATCH] add enums, add more stuff --- docs/api.rst | 94 +++++++++++++++++++++++++++++++++++++++++ requirements.txt | 1 + revolt/__init__.py | 2 +- revolt/asset.py | 51 ++++++++++++++++++---- revolt/channel.py | 41 +++++++++--------- revolt/enums.py | 37 ++++++++++++++++ revolt/errors.py | 6 +++ revolt/message.py | 13 +++--- revolt/server.py | 9 ++-- revolt/state.py | 5 +++ revolt/types/channel.py | 6 +-- revolt/types/server.py | 2 +- revolt/types/user.py | 14 ++++-- revolt/user.py | 29 ++++++++++--- revolt/websocket.py | 5 ++- 15 files changed, 263 insertions(+), 52 deletions(-) create mode 100644 revolt/enums.py diff --git a/docs/api.rst b/docs/api.rst index e80b658..09c0ba6 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -34,6 +34,25 @@ DMChannel .. autoclass:: DMChannel :members: +GroupDMChannel +~~~~~~~~~~ + +.. autoclass:: GroupDMChannel + :members: + +TextChannel +~~~~~~~~~~ + +.. autoclass:: TextChannel + :members: + +VoiceChannel +~~~~~~~~~~ + +.. autoclass:: VoiceChannel + :members: + + Embed ~~~~~~~~~~ @@ -88,3 +107,78 @@ User .. autoclass:: User :members: + +Enums +------ + +The api uses enums to say what variant of something is, these represent those enums + +All enums subclass `aenum.Enum`. + +.. class:: ChannelType + + Specifies the type of channel. + + .. attribute:: saved_message + + A private channel only you can access. + .. attribute:: direct_message + + A private direct message channel between you and another user + .. attribute:: group + + A private group channel for messages between a group of users + .. attribute:: text_channel + + A text channel in a server + .. attribute:: voice_channel + + A voice only channel + +.. class:: PresenceType + + Specifies what a users presence is + + .. attribute:: busy + + The user is busy and wont receive notification + .. attribute:: idle + + The user is idle + .. attribute:: invisible + + The user is invisible, you will never receive this, instead they will appear offline + .. attribute:: online + + The user is online + + .. attribute:: offline + + The user is offline or invisible + +.. attribute:: RelationshipType + + Specifies the relationship between two users + + .. attribute:: blocked + + You have blocked them + .. attribute:: blocked_other + + They have blocked you + .. attribute:: friend + + You are friends with them + .. attribute:: incoming_friend_request + + They are sending you a friend request + .. attribute:: none + + You have no relationship with them + .. attribute:: outgoing_friend_request + + You are sending them a friend request + + .. attribute:: user + + That user is yourself diff --git a/requirements.txt b/requirements.txt index a4ea331..dedee8e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,3 @@ aiohttp==3.7.4.post0 ulid-py==1.1.0 +aenum==3.1.0 diff --git a/revolt/__init__.py b/revolt/__init__.py index b134b8c..bccc4bf 100644 --- a/revolt/__init__.py +++ b/revolt/__init__.py @@ -1,5 +1,5 @@ from .asset import Asset -from .channel import DMChannel, Channel, GroupDMChannel, PartialTextChannel, SavedMessageChannel, VoiceChannel +from .channel import DMChannel, Channel, GroupDMChannel, SavedMessageChannel, VoiceChannel from .client import Client from .embed import Embed from .errors import HTTPError, RevoltError, ServerError diff --git a/revolt/asset.py b/revolt/asset.py index 907ccd8..fc45db2 100644 --- a/revolt/asset.py +++ b/revolt/asset.py @@ -2,6 +2,9 @@ from __future__ import annotations from typing import TYPE_CHECKING +from .errors import AutumnDisabled +from .enums import AssetType + if TYPE_CHECKING: from .types import File as FilePayload from .state import State @@ -12,22 +15,26 @@ class Asset: Attributes ----------- - id: str + id: :class:`str` The id of the asset - tag: str + tag: :class:`str` The tag of the asset, this corrasponds to where the asset is used - size: int + size: :class:`int` Amount of bytes in the file - filename: str + filename: :class:`str` The name of the file - height: Optional[int] + height: Optional[:class:`int`] The height of the file if it is an image or video - width: Optional[int] + width: Optional[:class:`int`] The width of the file if it is an image or video - content_type: str - The content type of the file + content_type: :class:`str` + The content type of the file + type: :class:`AssetType` + The type of asset it is """ def __init__(self, data: FilePayload, state: State): + self.state = state + self.id = data['_id'] self.tag = data['tag'] self.size = data['size'] @@ -44,6 +51,8 @@ class Asset: self.content_type = data["content_type"] + self.type = AssetType(metadata["type"]) + async def read(self) -> bytes: """Reads the files content into bytes""" ... @@ -57,3 +66,29 @@ class Asset: The file to write too. """ fp.write(await self.read()) + + @property + def url(self) -> str: + """Returns the url for the asset + + .. note:: This can error if autumn is disabled on the instance of revolt + + Returns + -------- + :class:`str` + The url + + Raises + ------- + :class:`AutumnDisabled` + Raises if autumn is disabled + + """ + enabled = self.state.api_info["features"]["autumn"]["enabled"] + + if not enabled: + raise AutumnDisabled + + base_url = self.state.api_info["features"]["autumn"]["url"] + + return f"{base_url}/{self.tag}/{self.id}" diff --git a/revolt/channel.py b/revolt/channel.py index 826c413..b6af6c0 100644 --- a/revolt/channel.py +++ b/revolt/channel.py @@ -1,12 +1,14 @@ from __future__ import annotations -from .messageable import Messageable +from typing import TYPE_CHECKING, cast -from typing import TYPE_CHECKING +from .messageable import Messageable +from .enums import ChannelType if TYPE_CHECKING: - from .types import Channel as ChannelPayload + from .types import Channel as ChannelPayload, SavedMessages as SavedMessagesPayload, DMChannel as DMChannelPayload, Group as GroupDMChannelPayload, TextChannel as TextChannelPayload from .state import State + from .user import User class Channel: """Base class for all channels @@ -15,7 +17,7 @@ class Channel: ----------- id: :class:`str` The id of the channel - channel_type: Literal['SavedMessage', 'DirectMessage', 'Group', 'TextChannel', 'VoiceChannel'] + channel_type: ChannelType The type of the channel server: Optional[:class:`Server`] The server the channel is part of @@ -23,39 +25,40 @@ class Channel: def __init__(self, data: ChannelPayload, state: State): self.state = state self.id = data["_id"] - self.channel_type = data["channel_type"] + self.channel_type = ChannelType(data["channel_type"]) self.server = None class SavedMessageChannel(Channel, Messageable): """The Saved Message Channel""" - def __init__(self, data: ChannelPayload, state: State): + def __init__(self, data: SavedMessagesPayload, state: State): super().__init__(data, state) class DMChannel(Channel, Messageable): """A DM channel""" - def __init__(self, data: ChannelPayload, state: State): + def __init__(self, data: DMChannelPayload, state: State): super().__init__(data, state) class GroupDMChannel(Channel, Messageable): """A group DM channel""" - def __init__(self, data: ChannelPayload, state: State): + def __init__(self, data: GroupDMChannelPayload, state: State): super().__init__(data, state) + self.recipients = cast(list[User], list(filter(bool, [state.get_user(user_id) for user_id in data["recipients"]]))) + self.name = data["name"] + self.owner = state.get_user(data["owner"]) class TextChannel(Channel, Messageable): """A text channel""" - def __init__(self, data: ChannelPayload, state: State): + def __init__(self, data: TextChannelPayload, state: State): super().__init__(data, state) Messageable.__init__(self, state) - -class PartialTextChannel(Messageable): - """A partial text channel, this will appear when the channel isnt cached""" - def __init__(self, channel_id: str, state: State): - super().__init__(state) - - self.id = channel_id - self.name = "Unknown Channel" - self.channel_type = "TextChannel" - self.server = None + + self.server = state.get_server(data["server"]) + self.name = data["name"] + self.description = data.get("description") + + last_message_id = data.get("last_message") + self.last_message = state.get_message(last_message_id) + self.last_message_id = last_message_id class VoiceChannel(Channel): """A voice channel""" diff --git a/revolt/enums.py b/revolt/enums.py new file mode 100644 index 0000000..9207166 --- /dev/null +++ b/revolt/enums.py @@ -0,0 +1,37 @@ +from typing import TYPE_CHECKING + +# typing doesnt understand aenum so im pretending its stdlib enum while type checking + +if TYPE_CHECKING: + import enum +else: + import aenum as enum + +class ChannelType(enum.Enum): + saved_message = "SavedMessage" + direct_message = "DirectMessage" + group = "Group" + text_channel = "TextChannel" + voice_channel = "VoiceChannel" + +class PresenceType(enum.Enum): + busy = "Busy" + idle = "Idle" + invisible = "Invisible" + online = "Online" + +class RelationshipType(enum.Enum): + blocked = "Blocked" + blocked_other = "BlockedOther" + friend = "Friend" + incoming_friend_request = "Incoming" + none = "None" + outgoing_friend_request = "Outgoing" + user = "User" + +class AssetType(enum.Enum): + image = "Image" + video = "Video" + text = "Text" + audio = "Audio" + file = "File" diff --git a/revolt/errors.py b/revolt/errors.py index 5594211..613f6dc 100644 --- a/revolt/errors.py +++ b/revolt/errors.py @@ -6,3 +6,9 @@ class HTTPError(RevoltError): class ServerError(RevoltError): "Internal server error" + +class FeatureDisabled(RevoltError): + """Base class for any any feature disabled""" + +class AutumnDisabled(FeatureDisabled): + """The autumn feature is disabled""" diff --git a/revolt/message.py b/revolt/message.py index 2a1c98e..52408a0 100644 --- a/revolt/message.py +++ b/revolt/message.py @@ -4,7 +4,7 @@ from typing import TYPE_CHECKING from .asset import Asset from .embed import Embed -from .channel import TextChannel, PartialTextChannel, Messageable +from .channel import Messageable if TYPE_CHECKING: from .state import State @@ -38,13 +38,16 @@ class Message: self.attachments = [Asset(attachment, state) for attachment in data.get("attachments", [])] self.embeds = [Embed.from_dict(embed) for embed in data.get("embeds", [])] - channel = state.get_channel(data["channel"]) or PartialTextChannel(data["channel"], state) + channel = state.get_channel(data["channel"]) assert isinstance(channel, Messageable) self.channel = channel self.server = self.channel and self.channel.server - if isinstance(self.channel, TextChannel) and self.server: - self.author = state.get_member(self.server.id, data["author"]) + if self.server: + author = state.get_member(self.server.id, data["author"]) else: - self.author = state.get_user(data["author"]) + author = state.get_user(data["author"]) + + assert author + self.author = author diff --git a/revolt/server.py b/revolt/server.py index d070934..3d9611e 100644 --- a/revolt/server.py +++ b/revolt/server.py @@ -1,16 +1,15 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Optional +from typing import TYPE_CHECKING, Optional, cast from .permissions import Permissions -from .channel import channel_factory +from .role import Role +from .channel import Channel if TYPE_CHECKING: from .types import Server as ServerPayload from .state import State from .member import Member - from .role import Role - from .channel import Channel class Server: """Represents a server @@ -33,7 +32,7 @@ class Server: self._members: dict[str, Member] = {} self._roles: dict[str, Role] = {role_id: Role(role, role_id, state) for role_id, role in data.get("roles", {}).items()} - channels = [channel_factory(channel, state) for channel in data.get("channels", [])] + channels = cast(list[Channel], list(filter(bool, [state.get_channel(channel_id) for channel_id in data["channels"]]))) self._channels: dict[str, Channel] = {channel.id: channel for channel in channels} @property diff --git a/revolt/state.py b/revolt/state.py index 966a397..33341e9 100644 --- a/revolt/state.py +++ b/revolt/state.py @@ -75,6 +75,11 @@ class State: self.messages.appendleft(message) return message + def get_message(self, message_id: str) -> Optional[Message]: + for msg in self.messages: + if msg.id == message_id: + return msg + async def fetch_all_server_members(self): for server_id in self.servers.keys(): data = await self.http.get_server_members(server_id) diff --git a/revolt/types/channel.py b/revolt/types/channel.py index 18e7380..0e68a76 100644 --- a/revolt/types/channel.py +++ b/revolt/types/channel.py @@ -16,7 +16,7 @@ class SavedMessages(_NonceChannel, BaseChannel): user: str channel_type: Literal["SavedMessage"] -class DirectMessage(_NonceChannel, BaseChannel): +class DMChannel(_NonceChannel, BaseChannel): active: bool recipients: list[str] last_message: Message @@ -42,7 +42,7 @@ class TextChannel(_NonceChannel, _TextChannelOptional, BaseChannel): server: str name: str description: str - last_message: Message + last_message: str channel_type: Literal["TextChannel"] class _VoiceChannelOptional(TypedDict, total=False): @@ -56,4 +56,4 @@ class VoiceChannel(_NonceChannel, _TextChannelOptional, BaseChannel): description: str channel_type: Literal["VoiceChannel"] -Channel = Union[SavedMessages, DirectMessage, Group, TextChannel, VoiceChannel] +Channel = Union[SavedMessages, DMChannel, Group, TextChannel, VoiceChannel] diff --git a/revolt/types/server.py b/revolt/types/server.py index 4cf93ca..059587d 100644 --- a/revolt/types/server.py +++ b/revolt/types/server.py @@ -28,5 +28,5 @@ class Server(TypedDict): _id: str owner: str name: str - channels: list[Channel] + channels: list[str] default_permissions: Permission diff --git a/revolt/types/user.py b/revolt/types/user.py index 12076f9..a7432de 100644 --- a/revolt/types/user.py +++ b/revolt/types/user.py @@ -5,19 +5,27 @@ from typing import TYPE_CHECKING, TypedDict, Literal if TYPE_CHECKING: from .file import File +Relation = Literal["Blocked", "BlockedOther", "Friend", "Incoming", "None", "Outgoing", "User"] + class UserBot(TypedDict): owner: str -class Status(TypedDict, total=False): +class _OptionalStatus(TypedDict, total=False): text: str + +class Status(_OptionalStatus): presence: Literal["Busy", "Idle", "Invisible", "Online"] +class UserRelation(TypedDict): + status: Relation + _id: str + class _OptionalUser(TypedDict, total=False): avatar: File - relations: list + relations: list[UserRelation] badges: int status: Status - relationship: Literal["Blocked", "BlockedOther", "Friend", "Incoming", "None", "Outgoing", "User"] + relationship: Relation online: bool flags: int bot: UserBot diff --git a/revolt/user.py b/revolt/user.py index 52c8480..0bfa909 100644 --- a/revolt/user.py +++ b/revolt/user.py @@ -1,10 +1,21 @@ from __future__ import annotations -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, NamedTuple, Optional + +from .asset import Asset +from .enums import RelationshipType, PresenceType if TYPE_CHECKING: from .state import State - from .types import User as UserPayload + from .types import User as UserPayload, UserRelation + +class Relation(NamedTuple): + type: RelationshipType + user: Optional["User"] + +class Status(NamedTuple): + text: Optional[str] + presence: PresenceType class User: """Represents a user @@ -25,6 +36,8 @@ class User: Whether or not the user is online flags: :class:`int` The user flags + avatar: :class:`Asset` + The users avatar if they have one """ __flattern_attributes__ = ("id", "name", "bot", "owner", "badges", "online", "flags") @@ -45,6 +58,12 @@ class User: self.online = data.get("online", False) self.flags = data.get("flags", 0) - # avatar - # relations - # relationship + avatar = data.get("avatar") + self.avatar = Asset(avatar, state) if avatar else None + + self.relations = [Relation(RelationshipType(relation["status"]), state.get_user(relation["_id"])) for relation in data.get("relations", [])] + relationship = data.get("relationship") + self.relationship = RelationshipType(relationship) if relationship else None + + status = data.get("status") + self.status = Status(status.get("text"), PresenceType(status["presence"])) if status else None diff --git a/revolt/websocket.py b/revolt/websocket.py index b9a9908..664e68e 100644 --- a/revolt/websocket.py +++ b/revolt/websocket.py @@ -32,6 +32,7 @@ class WebsocketHandler: self.dispatch = dispatch self.state = state self.websocket: aiohttp.ClientWebSocketResponse + self.loop = asyncio.get_running_loop() async def send_payload(self, payload: BasePayload): if use_msgpack: @@ -42,7 +43,7 @@ class WebsocketHandler: async def heartbeat(self): while not self.websocket.closed: logger.info("Sending hearbeat") - await self.send_payload({"type": "Ping"}) + await self.websocket.ping() await asyncio.sleep(15) async def send_authenticate(self): @@ -104,4 +105,4 @@ class WebsocketHandler: else: payload = json.loads(msg.data) - await self.handle_event(payload) + self.loop.create_task(self.handle_event(payload))