embed, but without fields yet and some other changes

This commit is contained in:
grey-cat-1908 2022-04-12 11:13:23 +03:00
parent 1ca2b593e4
commit cefbf9e7f7
7 changed files with 503 additions and 88 deletions

View file

@ -43,7 +43,7 @@ to handle it, which defaults to print a traceback and ignoring the exception.
.. note:: .. note::
It will not be received by :meth:`Client.wait_for`. It will not be received by :meth:`Client.wait_for`.
:param exception: Produced error. :param exception: Exception.
:type exception: :class:`Exception` :type exception: :class:`Exception`
.. function:: on_channel_create(channel) .. function:: on_channel_create(channel)
@ -112,7 +112,7 @@ to handle it, which defaults to print a traceback and ignoring the exception.
See the docs of :attr:`Intents.MESSAGE_CONTENT` for more information. See the docs of :attr:`Intents.MESSAGE_CONTENT` for more information.
:param message: The current message. :param message: The current message.
:type message: :class:`models.message.message.Message :type message: :class:`models.message.message.Message`
.. function:: on_shard_ready(shard_id) .. function:: on_shard_ready(shard_id)

View file

@ -55,6 +55,28 @@ class PrivilegedIntentsRequired(ClientException):
super().__init__(message.format(self.shard_id)) super().__init__(message.format(self.shard_id))
class EmbedFieldError(MelisaException, ValueError):
"""Occurs when an embed field is too large."""
@classmethod
def characters_from_desc(cls, field_type: str, current_size: int, max_size: int):
"""Create an instance by description.
Parameters
----------
field_type :class:`str`
The type/name of the field.
current_size :class:`int`
The current size of the field.
max_si :class:`int`
The maximum size of the field.
"""
return cls(
f"{field_type} can have maximum {max_size} characters."
f" (Current size: {current_size})"
)
class HTTPException(MelisaException): class HTTPException(MelisaException):
"""Occurs when an HTTP request operation fails.""" """Occurs when an HTTP request operation fails."""

View file

@ -18,6 +18,8 @@ from typing import (
) )
from ..message.message import Message from ..message.message import Message
from ...exceptions import EmbedFieldError
from ...models.message.embed import Embed
from ...utils import Snowflake, Timestamp from ...utils import Snowflake, Timestamp
from ...utils import APIModelBase from ...utils import APIModelBase
from ...utils.types import APINullable from ...utils.types import APINullable
@ -502,7 +504,9 @@ class MessageableChannel(Channel):
headers={"X-Audit-Log-Reason": reason}, headers={"X-Audit-Log-Reason": reason},
) )
async def send(self, content: str = None) -> Message: async def send(
self, content: str = None, *, embed: Embed = None, embeds: List[Embed] = None
) -> Message:
"""|coro| """|coro|
Sends a message to the destination with the content given. Sends a message to the destination with the content given.
@ -513,6 +517,10 @@ class MessageableChannel(Channel):
---------- ----------
content: Optional[:class:`str`] content: Optional[:class:`str`]
The content of the message to send. The content of the message to send.
embed: Optional[:class:`~melisa.models.message.embed.Embed`]
Embed
embeds: Optional[List[:class:`~melisa.models.message.embed.Embed`]]
List of embeds
Raises Raises
------- -------
@ -526,11 +534,22 @@ class MessageableChannel(Channel):
# ToDo: Add other parameters # ToDo: Add other parameters
if embeds is None:
embeds = []
content = str(content) if content is not None else None content = str(content) if content is not None else None
embeds.append(embed.to_dict()) if embed is not None else None
for _embed in embeds:
if embed.total_length() > 6000:
raise EmbedFieldError.characters_from_desc(
"Embed", embed.total_length(), 6000
)
return Message.from_dict( return Message.from_dict(
await self._http.post( await self._http.post(
f"/channels/{self.id}/messages", data={"content": content} f"/channels/{self.id}/messages",
data={"content": content, "embeds": embeds},
) )
) )

View file

@ -169,85 +169,6 @@ class SystemChannelFlags(IntEnum):
return self.value return self.value
class GuildFeatures(Enum):
"""Guild Features
Attributes
----------
ANIMATED_ICON:
Guild has access to set an animated guild icon
BANNER:
Guild has access to set a guild banner image
COMMERCE:
Guild has access to use commerce features (i.e. create store channels)
COMMUNITY:
Guild can enable welcome screen, Membership Screening,
stage channels and discovery, and receives community updates
DISCOVERABLE:
Guild is able to be discovered in the directory
FEATURABLE:
Guild is able to be featured in the directory
INVITE_SPLASH:
Guild has access to set an invite splash background
MEMBER_VERIFICATION_GATE_ENABLED:
Guild has enabled Membership Screening
MONETIZATION_ENABLED:
Guild has enabled monetization
MORE_STICKERS:
Guild has increased custom sticker slots
NEWS:
Guild has access to create news channels
PARTNERED:
Guild is partnered
PREVIEW_ENABLED:
Guild can be previewed before joining via Membership Screening or the directory
PRIVATE_THREADS:
Guild has access to create private threads
ROLE_ICONS:
Guild is able to set role icons
SEVEN_DAY_THREAD_ARCHIVE:
Guild has access to the seven day archive time for threads
THREE_DAY_THREAD_ARCHIVE:
Guild has access to the three day archive time for threads
TICKETED_EVENTS_ENABLED:
Guild has enabled ticketed events
VANITY_URL:
Guild has access to set a vanity URL
VERIFIED:
Guild is verified
VIP_REGIONS:
Guild has access to set 384kbps bitrate in voice (previously VIP voice servers)
WELCOME_SCREEN_ENABLED:
Guild has enabled the welcome screen
EXPOSED_TO_ACTIVITIES_WTP_EXPERIMENT:
Unkown. Found during testing. Not listed in Discord API docs.
"""
ANIMATED_ICON = "ANIMATED_ICON"
BANNER = "BANNER"
COMMERCE = "COMMERCE"
COMMUNITY = "COMMUNITY"
DISCOVERABLE = "DISCOVERABLE"
FEATURABLE = "FEATURABLE"
INVITE_SPLASH = "INVITE_SPLASH"
MEMBER_VERIFICATION_GATE_ENABLED = "MEMBER_VERIFICATION_GATE_ENABLED"
MONETIZATION_ENABLED = "MONETIZATION_ENABLED"
MORE_STICKERS = "MORE_STICKERS"
NEWS = "NEWS"
PARTNERED = "PARTNERED"
PREVIEW_ENABLED = "PREVIEW_ENABLED"
PRIVATE_THREADS = "PRIVATE_THREADS"
ROLE_ICONS = "ROLE_ICONS"
SEVEN_DAY_THREAD_ARCHIVE = "SEVEN_DAY_THREAD_ARCHIVE"
THREE_DAY_THREAD_ARCHIVE = "THREE_DAY_THREAD_ARCHIVE"
TICKETED_EVENTS_ENABLED = "TICKETED_EVENTS_ENABLED"
VANITY_URL = "VANITY_URL"
VERIFIED = "VERIFIED"
VIP_REGIONS = "VIP_REGIONS"
WELCOME_SCREEN_ENABLED = "WELCOME_SCREEN_ENABLED"
EXPOSED_TO_ACTIVITIES_WTP_EXPERIMENT = "EXPOSED_TO_ACTIVITIES_WTP_EXPERIMENT"
@dataclass(repr=False) @dataclass(repr=False)
class Guild(APIModelBase): class Guild(APIModelBase):
"""Guilds in Discord represent an isolated collection of users and channels, """Guilds in Discord represent an isolated collection of users and channels,
@ -289,7 +210,7 @@ class Guild(APIModelBase):
Default message notifications level Default message notifications level
explicit_content_filter: :class:`int` explicit_content_filter: :class:`int`
Explicit content filter level Explicit content filter level
features: APINullable[:class:`typing.Any`] features: APINullable[List[:class:`str`]]
Enabled guild features Enabled guild features
roles: APINullable[:class:`typing.Any`] roles: APINullable[:class:`typing.Any`]
Roles in the guild Roles in the guild
@ -388,7 +309,7 @@ class Guild(APIModelBase):
verification_level: APINullable[int] = None verification_level: APINullable[int] = None
default_message_notifications: APINullable[int] = None default_message_notifications: APINullable[int] = None
explicit_content_filter: APINullable[int] = None explicit_content_filter: APINullable[int] = None
features: APINullable[List[GuildFeatures]] = None features: APINullable[List[str]] = None
roles: APINullable[List] = None roles: APINullable[List] = None
emojis: APINullable[List] = None emojis: APINullable[List] = None
# TODO: Make a structures of emoji and role # TODO: Make a structures of emoji and role

View file

@ -1,6 +1,5 @@
# Copyright MelisaDev 2022 - Present # Copyright MelisaDev 2022 - Present
# Full MIT License can be found in `LICENSE.txt` at the project root. # Full MIT License can be found in `LICENSE.txt` at the project root.
from .message import MessageActivityType, MessageFlags, MessageType, Message from .message import *
from .embed import *
__all__ = ("MessageActivityType", "MessageFlags", "MessageType", "Message")

View file

@ -0,0 +1,396 @@
# Copyright MelisaDev 2022 - Present
# Full MIT License can be found in `LICENSE.txt` at the project root.
from __future__ import annotations
from dataclasses import dataclass
from datetime import datetime
from enum import Enum
from typing import List, Union, Optional
from melisa.exceptions import EmbedFieldError
from melisa.utils.api_model import APIModelBase, APINullable
from melisa.utils.timestamp import Timestamp
class EmbedType(Enum):
"""
Embed types are "loosely defined" and, for the most part,
are not used by our clients for rendering.
Embed attributes power what is rendered.
Embed types should be considered deprecated and might be removed in a future API version.
Attributes
__________
RICH:
Generic embed rendered from embed attributes
IMAGE
Image embed
VIDEO
Video embed
GIFV
Animated gif image embed rendered as a video embed
ARTICLE
Article embed
LINK
Link embed
"""
RICH = "rich"
IMAGE = "image"
VIDEO = "video"
GIFV = "gifv"
ARTICLE = "article"
LINK = "link"
@dataclass(repr=False)
class EmbedThumbnail(APIModelBase):
"""Representation of the Embed Thumbnail
Attributes
----------
url: :class:`str`
Source url of the thumbnail
proxy_url: Optional[:class:`str`]
A proxied url of the thumbnail
height: Optional[:class:`int`]
Height of the thumbnail
width: Optional[:class:`int`]
Width of the thumbnail
"""
url: str
proxy_url: APINullable[str] = None
height: APINullable[int] = None
width: APINullable[int] = None
@dataclass(repr=False)
class EmbedVideo(APIModelBase):
"""Representation of the Embed Video
Attributes
----------
url: Optional[:class:`str`]
Source url of the video
proxy_url: Optional[:class:`str`]
A proxied url of the video
height: Optional[:class:`int`]
Height of the video
width: Optional[:class:`int`]
Width of the video
"""
url: str
proxy_url: APINullable[str] = None
height: APINullable[int] = None
width: APINullable[int] = None
@dataclass(repr=False)
class EmbedImage(APIModelBase):
"""Representation of the Embed Image
Attributes
----------
url: :class:`str`
Source url of image (only supports http(s) and attachments)
proxy_url: Optional[:class:`str`]
A proxied url of the image
height: Optional[:class:`int`]
Height of the image
width: Optional[:class:`int`]
Width of the image
"""
url: str
proxy_url: APINullable[str] = None
height: APINullable[int] = None
width: APINullable[int] = None
@dataclass(repr=False)
class EmbedProvider(APIModelBase):
"""Representation of the Embed Provider
Attributes
----------
name: Optional[:class:`str`]
Name of provider
url: Optional[:class:`str`]
Url of provider
"""
name: APINullable[str] = None
url: APINullable[str] = None
@dataclass(repr=False)
class EmbedAuthor(APIModelBase):
"""Representation of the Embed Author
Attributes
----------
name: :class:`str`
Name of author
url: Optional[:class:`str`]
Url of author
icon_url: Optional[:class:`str`]
Url of author icon (only supports http(s) and attachments)
proxy_icon_url: Optional[:class:`str`]
A proxied url of author icon
"""
name: str
url: APINullable[str] = None
icon_url: APINullable[str] = None
proxy_icon_url: APINullable[str] = None
@dataclass(repr=False)
class EmbedFooter(APIModelBase):
"""Representation of the Embed Footer
Attributes
----------
text: :class:`str`
Footer text
icon_url: Optional[:class:`str`]
Url of footer icon (only supports http(s) and attachments)
proxy_icon_url: Optional[:class:`str`]
A proxied url of footer icon
"""
text: str
icon_url: APINullable[str] = None
proxy_icon_url: APINullable[str] = None
@dataclass(repr=False)
class EmbedField(APIModelBase):
"""Representation of the Embed Field
Attributes
----------
name: :class:`str`
Name of the field
value: :class:`str`
Value of the field
inline: Optional[:class:`bool`]
Whether or not this field should display inline
"""
name: str
value: str
inline: APINullable[str] = None
@dataclass(repr=False)
class Embed(APIModelBase):
# ToDo: Add fields set method
"""Represents an embed sent in with message within Discord.
Attributes
----------
title: Optional[:class:`str`]
Title of embed
type: Optional[:class:`~melisa.models.message.embed.EmbedType`]
Type of embed (always "rich" for webhook embeds)
description: Optional[:class:`str`]
Description of embed
color: Optional[:class:`int`]
Color code of the embed
fields: Optional[List[:class:`~melisa.models.message.embed.EmbedField`]]
Fields information.
footer: Optional[:class:`~melisa.models.message.embed.EmbedFooter`]
Footer information.
image: Optional[:class:`~melisa.models.message.embed.EmbedImage`]
Image information.
provider: Optional[:class:`~melisa.models.message.embed.EmbedProvider`]
Provider information.
thumbnail: Optional[:class:`~melisa.models.message.embed.EmbedThumbnail`]
Thumbnail information.
timestamp: Optional[:class:`~melisa.utils.timestamp.Timestamp`]
Timestamp of embed content
url: Optional[:class:`str`]
Url of embed
video: Optional[:class:`~melisa.models.message.embed.EmbedVideo`]
Video information.
"""
title: APINullable[str] = None
type: APINullable[EmbedType] = None
description: APINullable[str] = None
url: APINullable[str] = None
timestamp: APINullable[Timestamp] = None
color: APINullable[int] = None
footer: APINullable[EmbedFooter] = None
image: APINullable[EmbedImage] = None
thumbnail: APINullable[EmbedThumbnail] = None
video: APINullable[EmbedVideo] = None
provider: APINullable[EmbedProvider] = None
author: APINullable[EmbedAuthor] = None
fields: APINullable[List[EmbedField]] = None
def __post_init__(self):
if self.title and len(self.title) > 256:
raise EmbedFieldError.characters_from_desc(
"Embed Title",
len(self.title),
256,
)
if self.description and len(self.description) > 4096:
raise EmbedFieldError.characters_from_desc(
"Embed Description", len(self.description), 4096
)
if self.fields and len(self.fields) > 25:
raise EmbedFieldError("""You can't set more than 25 embed fields!""")
def set_timestamp(self, time: Union[Timestamp, datetime]) -> Embed:
"""Sets timestamp in the supported by discord format.
Parameters
----------
time: :class:`~melisa.utils.timestamp.Timestamp`
The datetime to set the timestamp to.
Returns
-------
:class:`~,e;osa.models.message.embed.Embed`
The new embed object.
"""
self.timestamp = time.isoformat()
return self
def set_author(
self,
name: str,
*,
url: Optional[str] = None,
icon_url: Optional[str] = None,
proxy_icon_url: Optional[str] = None,
) -> Embed:
"""Set the author for the embed.
Parameters
----------
name: :class:`str`
Name of author
url: Optional[:class:`str`]
Url of author icon (only supports http(s) and attachments)
icon_url: Optional[:class:`str`]
Url of author
proxy_icon_url: Optional[:class:`str`]
A proxied url of author icon
Returns
-------
:class:`~melisa.models.message.embed.Embed`
Updated embed.
"""
self.author = EmbedAuthor(
name=name,
url=url,
icon_url=icon_url,
proxy_icon_url=proxy_icon_url,
)
return self
def set_image(self, url: str, *, proxy_url: Optional[str] = None) -> Embed:
"""Set the image for the embed.
Parameters
----------
url: :class:`str`
Source url of image (only supports http(s) and attachments)
proxy_url: Optional[:class:`str`]
A proxied url of the image
Returns
-------
:class:`~melisa.models.message.embed.Embed`
Updated embed.
"""
self.image = EmbedImage(url=url, proxy_url=proxy_url)
return self
def set_thumbnail(self, url: str, *, proxy_url: Optional[str] = None) -> Embed:
"""Set the thumbnail for the embed.
Parameters
----------
url: :class:`str`
Source url of thumbnail (only supports http(s) and attachments)
proxy_url: Optional[:class:`str`]
A proxied url of the thumbnail
Returns
-------
:class:`~melisa.models.message.embed.Embed`
Updated embed.
"""
self.thumbnail = EmbedThumbnail(url=url, proxy_url=proxy_url)
return self
def set_footer(
self,
text: str,
*,
icon_url: Optional[str] = None,
proxy_icon_url: Optional[str] = None,
) -> Embed:
"""
Sets the embed footer.
Parameters
----------
text: :class:`str`
Footer text
icon_url: Optional[:class:`str`]
Url of footer icon (only supports http(s) and attachments)
proxy_icon_url: Optional[:class:`str`]
A proxied url of footer icon
Returns
-------
:class:`~melisa.models.message.embed.Embed`
Updated embed.
"""
self.footer = EmbedFooter(
text=text, icon_url=icon_url, proxy_icon_url=proxy_icon_url
)
return self
def total_length(self) -> int:
"""Get the total character count of the embed.
Returns
-------
:class:`int`
The total character count of this embed, including title, description,
fields, footer, and author combined.
"""
total = len(self.title or "") + len(self.description or "")
if self.fields:
for field in self.fields:
total += len(field.name) + len(field.value)
if self.footer and self.footer.text:
total += len(self.footer.text)
if self.author and self.author.name:
total += len(self.author.name)
return total

58
tests/test_embeds.py Normal file
View file

@ -0,0 +1,58 @@
import datetime
from melisa import Embed, Timestamp
dict_embed = {
'title': 'my title',
'description': 'simple description',
'timestamp': datetime.datetime.utcfromtimestamp(1649748784).isoformat(),
'footer': {
'text': 'cool footer text'
},
'author': {
'name': 'best author'
},
}
class TestEmbed:
def test_total_length_when_embed_is_empty(self):
embed = Embed()
assert embed.total_length() == 0
def test_total_length_when_title_is_none(self):
embed = Embed(title=None)
assert embed.total_length() == 0
def test_total_length_title(self):
embed = Embed(title="my title")
assert embed.total_length() == 8
def test_total_length_when_description_is_none(self):
embed = Embed(description=None)
assert embed.total_length() == 0
def test_total_length_description(self):
embed = Embed(description="simple description")
assert embed.total_length() == 18
def test_total_length_author_name(self):
embed = Embed().set_author(name="best author")
assert embed.total_length() == 11
def test_total_length_footer_text(self):
embed = Embed().set_footer(text="cool footer text")
assert embed.total_length() == 16
def test_total_length_all(self):
embed = Embed(title="my title", description="simple description")
embed.set_author(name="best author")
embed.set_footer(text="cool footer text")
assert embed.total_length() == 53
def test_comparing_embeds(self):
embed = Embed(title="my title", description="simple description")
embed.set_author(name="best author")
embed.set_footer(text="cool footer text")
embed.set_timestamp(Timestamp.parse(1649748784))
assert embed.to_dict() == dict_embed