2 Commits

Author SHA1 Message Date
6c7555c26c Fix some docs 2021-10-03 17:00:16 +01:00
a8605c8c4a Implement role icons 2021-10-03 15:45:06 +01:00
5 changed files with 90 additions and 29 deletions

View File

@ -245,6 +245,15 @@ class Asset(AssetMixin):
animated=animated, animated=animated,
) )
@classmethod
def _from_role_icon(cls, state, role_id: int, role_hash: str) -> Asset:
return cls(
state,
url=f"{cls.BASE}/role-icons/{role_id}/{role_hash}.png",
key=role_hash,
animated=False,
)
def __str__(self) -> str: def __str__(self) -> str:
return self._url return self._url

View File

@ -153,38 +153,38 @@ class GatewayRatelimiter:
await asyncio.sleep(delta) await asyncio.sleep(delta)
class KeepAliveHandler: class KeepAliveHandler(threading.Thread):
def __init__(self, *, ws: DiscordWebSocket, shard_id: int = None, interval: float = None) -> None: def __init__(self, *args: Any, **kwargs: Any) -> None:
ws = kwargs.pop("ws")
interval = kwargs.pop("interval", None)
shard_id = kwargs.pop("shard_id", None)
threading.Thread.__init__(self, *args, **kwargs)
self.ws: DiscordWebSocket = ws self.ws: DiscordWebSocket = ws
self.shard_id: Optional[int] = shard_id self._main_thread_id: int = ws.thread_id
self.interval: Optional[float] = interval self.interval: Optional[float] = interval
self.heartbeat_timeout: float = self.ws._max_heartbeat_timeout self.daemon: bool = True
self.shard_id: Optional[int] = shard_id
self.msg: str = "Keeping shard ID %s websocket alive with sequence %s." self.msg: str = "Keeping shard ID %s websocket alive with sequence %s."
self.block_msg: str = "Shard ID %s heartbeat blocked for more than %s seconds." self.block_msg: str = "Shard ID %s heartbeat blocked for more than %s seconds."
self.behind_msg: str = "Can't keep up, shard ID %s websocket is %.1fs behind." self.behind_msg: str = "Can't keep up, shard ID %s websocket is %.1fs behind."
self._stop_ev: asyncio.Event = asyncio.Event() self._stop_ev: threading.Event = threading.Event()
self._last_ack: float = time.perf_counter()
self._last_send: float = time.perf_counter() self._last_send: float = time.perf_counter()
self._last_recv: float = time.perf_counter() self._last_recv: float = time.perf_counter()
self._last_ack: float = time.perf_counter()
self.latency: float = float("inf") self.latency: float = float("inf")
self.heartbeat_timeout: float = ws._max_heartbeat_timeout
async def run(self) -> None: def run(self) -> None:
while True: while not self._stop_ev.wait(self.interval):
try:
await asyncio.wait_for(self._stop_ev.wait(), timeout=self.interval)
except asyncio.TimeoutError:
pass
else:
return
if self._last_recv + self.heartbeat_timeout < time.perf_counter(): if self._last_recv + self.heartbeat_timeout < time.perf_counter():
_log.warning( _log.warning(
"Shard ID %s has stopped responding to the gateway. Closing and restarting.", self.shard_id "Shard ID %s has stopped responding to the gateway. Closing and restarting.", self.shard_id
) )
coro = self.ws.close(4000)
f = asyncio.run_coroutine_threadsafe(coro, loop=self.ws.loop)
try: try:
await self.ws.close(4000) f.result()
except Exception: except Exception:
_log.exception("An error occurred while stopping the gateway. Ignoring.") _log.exception("An error occurred while stopping the gateway. Ignoring.")
finally: finally:
@ -193,18 +193,24 @@ class KeepAliveHandler:
data = self.get_payload() data = self.get_payload()
_log.debug(self.msg, self.shard_id, data["d"]) _log.debug(self.msg, self.shard_id, data["d"])
coro = self.ws.send_heartbeat(data)
f = asyncio.run_coroutine_threadsafe(coro, loop=self.ws.loop)
try: try:
# block until sending is complete # block until sending is complete
total = 0 total = 0
while True: while True:
try: try:
await asyncio.wait_for(self.ws.send_heartbeat(data), timeout=10) f.result(10)
break break
except asyncio.TimeoutError: except concurrent.futures.TimeoutError:
total += 10 total += 10
try:
stack = "".join(traceback.format_stack()) frame = sys._current_frames()[self._main_thread_id]
msg = f"{self.block_msg}\nLoop traceback (most recent call last):\n{stack}" except KeyError:
msg = self.block_msg
else:
stack = "".join(traceback.format_stack(frame))
msg = f"{self.block_msg}\nLoop thread traceback (most recent call last):\n{stack}"
_log.warning(msg, self.shard_id, total) _log.warning(msg, self.shard_id, total)
except Exception: except Exception:
@ -219,10 +225,6 @@ class KeepAliveHandler:
"d": self.ws.sequence, # type: ignore "d": self.ws.sequence, # type: ignore
} }
def start(self) -> None:
self.ws.loop.create_task(self.run())
def stop(self) -> None: def stop(self) -> None:
self._stop_ev.set() self._stop_ev.set()

View File

@ -2448,6 +2448,8 @@ class Guild(Hashable):
colour: Union[Colour, int] = ..., colour: Union[Colour, int] = ...,
hoist: bool = ..., hoist: bool = ...,
mentionable: bool = ..., mentionable: bool = ...,
icon: bytes = ...,
emoji: str = ...,
) -> Role: ) -> Role:
... ...
@ -2461,6 +2463,8 @@ class Guild(Hashable):
color: Union[Colour, int] = ..., color: Union[Colour, int] = ...,
hoist: bool = ..., hoist: bool = ...,
mentionable: bool = ..., mentionable: bool = ...,
icon: bytes = ...,
emoji: str = ...,
) -> Role: ) -> Role:
... ...
@ -2473,6 +2477,8 @@ class Guild(Hashable):
colour: Union[Colour, int] = MISSING, colour: Union[Colour, int] = MISSING,
hoist: bool = MISSING, hoist: bool = MISSING,
mentionable: bool = MISSING, mentionable: bool = MISSING,
icon: bytes = MISSING,
emoji: str = MISSING,
reason: Optional[str] = None, reason: Optional[str] = None,
) -> Role: ) -> Role:
"""|coro| """|coro|
@ -2502,6 +2508,10 @@ class Guild(Hashable):
mentionable: :class:`bool` mentionable: :class:`bool`
Indicates if the role should be mentionable by others. Indicates if the role should be mentionable by others.
Defaults to ``False``. Defaults to ``False``.
emoji: :class:`str`
The unicode emoji that is shown next to users with the role.
icon: :class:`bytes`
A custom image that is shown next to users with the role.
reason: Optional[:class:`str`] reason: Optional[:class:`str`]
The reason for creating this role. Shows up on the audit log. The reason for creating this role. Shows up on the audit log.
@ -2540,6 +2550,12 @@ class Guild(Hashable):
if name is not MISSING: if name is not MISSING:
fields["name"] = name fields["name"] = name
if emoji is not MISSING:
fields["unicode_emoji"] = emoji
if icon is not MISSING:
fields["icon"] = utils._bytes_to_base64_data(icon)
data = await self._state.http.create_role(self.id, reason=reason, **fields) data = await self._state.http.create_role(self.id, reason=reason, **fields)
role = Role(guild=self, data=data, state=self._state) role = Role(guild=self, data=data, state=self._state)

View File

@ -23,13 +23,14 @@ DEALINGS IN THE SOFTWARE.
""" """
from __future__ import annotations from __future__ import annotations
from typing import Any, Dict, List, Optional, TypeVar, Union, overload, TYPE_CHECKING from typing import Any, Dict, List, Optional, TypeVar, Union, TYPE_CHECKING
from .asset import Asset
from .permissions import Permissions from .permissions import Permissions
from .errors import InvalidArgument from .errors import InvalidArgument
from .colour import Colour from .colour import Colour
from .mixins import Hashable from .mixins import Hashable
from .utils import snowflake_time, _get_as_snowflake, MISSING from .utils import snowflake_time, _get_as_snowflake, MISSING, _bytes_to_base64_data
__all__ = ( __all__ = (
"RoleTags", "RoleTags",
@ -158,7 +159,15 @@ class Role(Hashable):
guild: :class:`Guild` guild: :class:`Guild`
The guild the role belongs to. The guild the role belongs to.
hoist: :class:`bool` hoist: :class:`bool`
Indicates if the role will be displayed separately from other members. Indicates if the role will be displayed separately from other members.
icon: Optional[:class:`Asset`]
A custom image that is shown next to users with the role.
.. versionadded:: 2.0
emoji: Optional[:class:`str`]
The unicode emoji that is shown next to users with the role.
.. versionadded:: 2.0
position: :class:`int` position: :class:`int`
The position of the role. This number is usually positive. The bottom The position of the role. This number is usually positive. The bottom
role has a position of 0. role has a position of 0.
@ -191,6 +200,8 @@ class Role(Hashable):
"hoist", "hoist",
"guild", "guild",
"tags", "tags",
"_icon",
"emoji",
"_state", "_state",
) )
@ -251,6 +262,8 @@ class Role(Hashable):
self.position: int = data.get("position", 0) self.position: int = data.get("position", 0)
self._colour: int = data.get("color", 0) self._colour: int = data.get("color", 0)
self.hoist: bool = data.get("hoist", False) self.hoist: bool = data.get("hoist", False)
self.emoji: Optional[str] = data.get("unicode_emoji")
self._icon: Optional[str] = data.get("icon")
self.managed: bool = data.get("managed", False) self.managed: bool = data.get("managed", False)
self.mentionable: bool = data.get("mentionable", False) self.mentionable: bool = data.get("mentionable", False)
self.tags: Optional[RoleTags] self.tags: Optional[RoleTags]
@ -318,6 +331,13 @@ class Role(Hashable):
""":class:`str`: Returns a string that allows you to mention a role.""" """:class:`str`: Returns a string that allows you to mention a role."""
return f"<@&{self.id}>" return f"<@&{self.id}>"
@property
def icon(self) -> Optional[Asset]:
"""Optional[:class:`Asset`]: Returns the custom icon shown next to users with the role, if it exists."""
if self._icon is None:
return
return Asset._from_role_icon(self._state, self.id, self._icon)
@property @property
def members(self) -> List[Member]: def members(self) -> List[Member]:
"""List[:class:`Member`]: Returns all the members with this role.""" """List[:class:`Member`]: Returns all the members with this role."""
@ -361,6 +381,8 @@ class Role(Hashable):
hoist: bool = MISSING, hoist: bool = MISSING,
mentionable: bool = MISSING, mentionable: bool = MISSING,
position: int = MISSING, position: int = MISSING,
icon: bytes = MISSING,
emoji: str = MISSING,
reason: Optional[str] = MISSING, reason: Optional[str] = MISSING,
) -> Optional[Role]: ) -> Optional[Role]:
"""|coro| """|coro|
@ -393,6 +415,10 @@ class Role(Hashable):
position: :class:`int` position: :class:`int`
The new role's position. This must be below your top role's The new role's position. This must be below your top role's
position or it will fail. position or it will fail.
emoji: :class:`str`
The unicode emoji that is shown next to users with the role.
icon: :class:`bytes`
A custom image that is shown next to users with the role.
reason: Optional[:class:`str`] reason: Optional[:class:`str`]
The reason for editing this role. Shows up on the audit log. The reason for editing this role. Shows up on the audit log.
@ -436,6 +462,12 @@ class Role(Hashable):
if mentionable is not MISSING: if mentionable is not MISSING:
payload["mentionable"] = mentionable payload["mentionable"] = mentionable
if emoji is not MISSING:
payload["unicode_emoji"] = emoji
if icon is not MISSING:
payload["icon"] = _bytes_to_base64_data(icon)
data = await self._state.http.edit_role(self.guild.id, self.id, reason=reason, **payload) data = await self._state.http.edit_role(self.guild.id, self.id, reason=reason, **payload)
return Role(guild=self.guild, data=data, state=self._state) return Role(guild=self.guild, data=data, state=self._state)

View File

@ -29,7 +29,9 @@ from .snowflake import Snowflake
class _RoleOptional(TypedDict, total=False): class _RoleOptional(TypedDict, total=False):
icon: str
tags: RoleTags tags: RoleTags
unicode_emoji: str
class Role(_RoleOptional): class Role(_RoleOptional):