Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Localisation support. #686

Merged
merged 26 commits into from
Apr 2, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
7d379f4
chore: release 4.1.0 (#542)
i0bs Feb 24, 2022
210573d
docs: add migration docs for 4.1
i0bs Feb 25, 2022
b2c1bad
docs: detail message content intent in migration
i0bs Feb 25, 2022
2afad6f
Merge branch 'unstable' into stable
i0bs Feb 25, 2022
ba46278
Merge branch 'unstable' into stable
i0bs Feb 25, 2022
a19ec99
Merge branch 'unstable' into stable
i0bs Feb 25, 2022
511dfe8
ADMIN/chore: finish catchup from rebase (#573)
i0bs Feb 25, 2022
6cc6333
ADMIN/chore: finish catchup from rebase (#573) (#574)
i0bs Feb 25, 2022
d443d7d
chore: pre-release 4.1.1-beta.1 (#603)
i0bs Mar 1, 2022
711ae0f
chore!: pre-release 4.1.1-beta.2 (#652)
i0bs Mar 23, 2022
d6f28b7
feat: Implement Localisation for name and description of application …
FayeDel Mar 29, 2022
f2830cb
feat: Implemented HTTP 50x error tracking, per-route exhaust ratelimi…
FayeDel Mar 29, 2022
460328a
refactor: Delete extra Locale object, tweak current Locale object acc…
FayeDel Mar 29, 2022
7ee564c
feat: Implement localisation support for Options and Choices, include…
FayeDel Mar 29, 2022
ea95732
fix: Fix listener/command invocation on extension reload/removal.
FayeDel Mar 29, 2022
8873ada
fix: Fix timestamp assignment on Embed objects on non-declaration.
FayeDel Mar 29, 2022
a52cf8e
chore: Repoint OpenCollective URL.
FayeDel Mar 29, 2022
c8af8bd
fix: Adjust Locale object support for .pyi headers.
FayeDel Mar 31, 2022
2dcf18c
Merge branch 'interactions-py:stable' into unstable_core_3
FayeDel Mar 31, 2022
c03c0ab
Merge remote-tracking branch 'origin/unstable' into unstable
FayeDel Mar 31, 2022
0ac9bc2
chore: attempt of rebase
FayeDel Mar 31, 2022
9ce0568
fix!: Fix circular import from commit c54a35c.
FayeDel Apr 2, 2022
33d2f3f
feat: Implement new guild ban parameter requirement.
FayeDel Apr 2, 2022
a611a27
feat: Extend __check_command to name localisations per command/option
FayeDel Apr 2, 2022
11e5a51
refactor: Refactor Embed timestamp definition by relying on __setattr__
FayeDel Apr 2, 2022
d7d218e
revert: Partially revert f2830cb by removing 50x error tracking, refa…
FayeDel Apr 2, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/FUNDING.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# These are supported funding model platforms

github: [goverfl0w]
open_collective: discordinteractions
open_collective: interactions-py
20 changes: 2 additions & 18 deletions interactions/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,22 +9,6 @@

(c) 2021 interactions-py.
"""
from .api.models.channel import * # noqa: F401 F403
from .api.models.flags import * # noqa: F401 F403
from .api.models.guild import * # noqa: F401 F403
from .api.models.gw import * # noqa: F401 F403
from .api.models.member import * # noqa: F401 F403
from .api.models.message import * # noqa: F401 F403
from .api.models.misc import * # noqa: F401 F403
from .api.models.presence import * # noqa: F401 F403
from .api.models.role import * # noqa: F401 F403
from .api.models.team import * # noqa: F401 F403
from .api.models.user import * # noqa: F401 F403
from .client import * # noqa: F401 F403 isort: skip
from .api import * # noqa: F401 F403
from .base import * # noqa: F401 F403
from .client.bot import * # noqa: F401 F403
from .client.context import * # noqa: F401 F403
from .client.decor import * # noqa: F401 F403
from .client.enums import * # noqa: F401 F403
from .client.models.command import * # noqa: F401 F403
from .client.models.component import * # noqa: F401 F403
from .client.models.misc import * # noqa: F401 F403
7 changes: 2 additions & 5 deletions interactions/api/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,11 @@
interactions.api

This section of the library maintains and
handles all of the Gateway and HTTP
work.
handles all the Gateway and HTTP work.
"""
from ..base import * # noqa: F401 F403
from .cache import * # noqa: F401 F403
from .enums import * # noqa: F401 F403
from .error import * # noqa: F401 F403
from .gateway.client import * # noqa: F401 F403
from .gateway.heartbeat import * # noqa: F401 F403
from .gateway import * # noqa: F401 F403
from .http import * # noqa: F401 F403
from .models import * # noqa: F401 F403
2 changes: 1 addition & 1 deletion interactions/api/gateway/heartbeat.pyi
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from asyncio import AbstractEventLoop, Event

class _Heartbeat:
class _Heartbeat():
event: Event
delay: float
def __init__(self, loop: AbstractEventLoop) -> None: ...
25 changes: 23 additions & 2 deletions interactions/api/http/guild.py
Original file line number Diff line number Diff line change
Expand Up @@ -507,14 +507,35 @@ async def remove_guild_ban(
reason=reason,
)

async def get_guild_bans(self, guild_id: int) -> List[dict]:
async def get_guild_bans(
self,
guild_id: int,
limit: Optional[int] = 1000,
before: Optional[int] = None,
after: Optional[int] = None,
) -> List[dict]:
"""
Gets a list of banned users.

.. note::
If both ``before`` and ``after`` are provided, only ``before`` is respected.

:param guild_id: Guild ID snowflake.
:param limit: Number of users to return. Defaults to 1000.
:param before: Consider only users before the given User ID snowflake.
:param after: Consider only users after the given User ID snowflake.
:return: A list of banned users.
"""
return await self._req.request(Route("GET", f"/guilds/{guild_id}/bans"))

params = {}
if limit is not None:
params["limit"] = limit
if before:
params["before"] = before
if after:
params["after"] = after

return await self._req.request(Route("GET", f"/guilds/{guild_id}/bans"), params=params)

async def get_user_ban(self, guild_id: int, user_id: int) -> Optional[dict]:
"""
Expand Down
15 changes: 11 additions & 4 deletions interactions/api/http/request.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,8 @@ async def request(self, route: Route, **kwargs) -> Optional[Any]:
"""

kwargs["headers"] = {**self._headers, **kwargs.get("headers", {})}
kwargs["headers"]["Content-Type"] = "application/json"
if kwargs.get("json"):
kwargs["headers"]["Content-Type"] = "application/json"

reason = kwargs.pop("reason", None)
if reason:
Expand Down Expand Up @@ -165,22 +166,28 @@ async def request(self, route: Route, **kwargs) -> Optional[Any]:
# This "redundant" debug line is for debug use and tracing back the error codes.

raise HTTPException(data["code"], message=data["message"])
elif remaining and not int(remaining):
if response.status == 429:

if response.status == 429:
if not is_global:
log.warning(
f"The HTTP client has encountered a per-route ratelimit. Locking down future requests for {reset_after} seconds."
)
_limiter.reset_after = reset_after
await asyncio.sleep(_limiter.reset_after)
continue
elif is_global:
else:
log.warning(
f"The HTTP client has encountered a global ratelimit. Locking down future requests for {reset_after} seconds."
)
self._global_lock.reset_after = reset_after
self._loop.call_later(
self._global_lock.reset_after, self._global_lock.lock.release
)
elif int(remaining) == 0:
log.warning(
f"The HTTP client has exhausted a per-route ratelimit. Locking route for {reset_after} seconds."
)
self._loop.call_later(reset_after, _limiter.release_lock())

log.debug(f"RETURN {response.status}: {dumps(data, indent=4, sort_keys=True)}")

Expand Down
2 changes: 1 addition & 1 deletion interactions/api/models/gw.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from datetime import datetime
from typing import List, Optional, Union

from ...models.component import ActionRow, Button, SelectMenu
from ...client.models.component import ActionRow, Button, SelectMenu
from .channel import Channel, ThreadMember
from .member import Member
from .message import Embed, Emoji, Message, MessageInteraction, Sticker
Expand Down
1 change: 1 addition & 0 deletions interactions/api/models/member.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ class Member(DictSerializerMixin):
"permissions",
"communication_disabled_until",
"hoisted_role",
"flags",
"_client",
)

Expand Down
3 changes: 2 additions & 1 deletion interactions/api/models/member.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ class Member(DictSerializerMixin):
pending: Optional[bool]
permissions: Optional[Permissions]
communication_disabled_until: Optional[datetime.isoformat]
hoisted_role: Any # TODO: post-v4: Investigate what this is for when documented by Discord.
hoisted_role: Any # TODO: Investigate what this is for when documented by Discord.
flags: int # TODO: Investigate what this is for when documented by Discord.
def __init__(self, **kwargs): ...
def __repr__(self) -> str: ...
@property
Expand Down
11 changes: 5 additions & 6 deletions interactions/api/models/message.py
Original file line number Diff line number Diff line change
Expand Up @@ -1031,11 +1031,11 @@ class Embed(DictSerializerMixin):

def __init__(self, **kwargs):
super().__init__(**kwargs)
self.timestamp = (
datetime.fromisoformat(self._json.get("timestamp"))
if self._json.get("timestamp")
else datetime.utcnow()
)
if isinstance(self._json.get("timestamp"), str):
self.timestamp = datetime.fromisoformat(
self._json.get("timestamp")
) # readability on non `_json` attr.

self.footer = EmbedFooter(**self.footer) if isinstance(self.footer, dict) else self.footer
self.image = EmbedImageStruct(**self.image) if isinstance(self.image, dict) else self.image
self.thumbnail = (
Expand All @@ -1053,7 +1053,6 @@ def __init__(self, **kwargs):
if self._json.get("fields")
else None
)

# (Complete partial fix.)
# The issue seems to be that this itself is not updating
# JSON result correctly. After numerous attempts I seem to
Expand Down
60 changes: 57 additions & 3 deletions interactions/client/bot.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
from ..base import get_logger
from .decor import command
from .decor import component as _component
from .enums import ApplicationCommandType, OptionType
from .enums import ApplicationCommandType, Locale, OptionType
from .models.command import ApplicationCommand, Option
from .models.component import Button, Modal, SelectMenu

Expand Down Expand Up @@ -397,6 +397,17 @@ def __check_sub_group(_sub_group: Option):
raise InteractionException(
11, message="Descriptions must be less than 100 characters."
)
if (
_sub_group.name_localizations is not MISSING
and _sub_group.name_localizations is not None
):
for __name in command.name_localizations.values():
if not re.fullmatch(reg, __name):
raise InteractionException(
11,
message=f"The sub command group name does not match the regex for valid names ('{regex}')",
)

if not _sub_group.options:
raise InteractionException(11, message="sub command groups must have subcommands!")
if len(_sub_group.options) > 25:
Expand Down Expand Up @@ -432,6 +443,17 @@ def __check_sub_command(_sub_command: Option, _sub_group: Option = MISSING):
raise InteractionException(
11, message="Descriptions must be less than 100 characters."
)
if (
_sub_command.name_localizations is not MISSING
and _sub_command.name_localizations is not None
):
for __name in command.name_localizations.values():
if not re.fullmatch(reg, __name):
raise InteractionException(
11,
message=f"The sub command name does not match the regex for valid names ('{regex}')",
)

if _sub_command.options is not MISSING and _sub_command.options:
if len(_sub_command.options) > 25:
raise InteractionException(
Expand Down Expand Up @@ -478,6 +500,14 @@ def __check_options(_option: Option, _names: list, _sub_command: Option = MISSIN
raise InteractionException(
11, message="You must not have two options with the same name in a command!"
)
if _option.name_localizations is not MISSING and _option.name_localizations is not None:
for __name in _option.name_localizations.values():
if not re.fullmatch(reg, __name):
raise InteractionException(
11,
message=f"The option name does not match the regex for valid names ('{regex}')",
)

_names.append(_option.name)

def __check_coro():
Expand Down Expand Up @@ -535,6 +565,14 @@ def __check_coro():
elif command.description is not MISSING and len(command.description) > 100:
raise InteractionException(11, message="Descriptions must be less than 100 characters.")

if command.name_localizations is not MISSING and command.name_localizations is not None:
for __name in command.name_localizations.values():
if not re.fullmatch(reg, __name):
raise InteractionException(
11,
message=f"One of your command name localisations does not match the regex for valid names ('{regex}')",
)

if command.options and command.options is not MISSING:
if len(command.options) > 25:
raise InteractionException(
Expand Down Expand Up @@ -570,6 +608,8 @@ def command(
options: Optional[
Union[Dict[str, Any], List[Dict[str, Any]], Option, List[Option]]
] = MISSING,
name_localizations: Optional[Dict[Union[str, Locale], str]] = MISSING,
description_localizations: Optional[Dict[Union[str, Locale], str]] = MISSING,
default_permission: Optional[bool] = MISSING,
) -> Callable[..., Any]:
"""
Expand Down Expand Up @@ -609,6 +649,10 @@ async def message_command(ctx):
:type options: Optional[Union[Dict[str, Any], List[Dict[str, Any]], Option, List[Option]]]
:param default_permission?: The default permission of accessibility for the application command. Defaults to ``True``.
:type default_permission: Optional[bool]
:param name_localizations?: The dictionary of localization for the ``name`` field. This enforces the same restrictions as the ``name`` field.
:param name_localizations: Optional[Dict[Union[str, Locale], str]]
:param description_localizations?: The dictionary of localization for the ``description`` field. This enforces the same restrictions as the ``description`` field.
:param description_localizations: Optional[Dict[Union[str, Locale], str]]
:return: A callable response.
:rtype: Callable[..., Any]
"""
Expand All @@ -622,6 +666,8 @@ def decorator(coro: Coroutine) -> Callable[..., Any]:
scope=scope,
options=options,
default_permission=default_permission,
name_localizations=name_localizations,
description_localizations=description_localizations,
)
self.__check_command(command=ApplicationCommand(**commands[0]), coro=coro)

Expand Down Expand Up @@ -650,6 +696,7 @@ def message_command(
name: str,
scope: Optional[Union[int, Guild, List[int], List[Guild]]] = MISSING,
default_permission: Optional[bool] = MISSING,
name_localizations: Optional[Dict[Union[str, Locale], Any]] = MISSING,
) -> Callable[..., Any]:
"""
A decorator for registering a message context menu to the Discord API,
Expand All @@ -673,6 +720,8 @@ async def context_menu_name(ctx):
:type scope: Optional[Union[int, Guild, List[int], List[Guild]]]
:param default_permission?: The default permission of accessibility for the application command. Defaults to ``True``.
:type default_permission: Optional[bool]
:param name_localizations?: The dictionary of localization for the ``name`` field. This enforces the same restrictions as the ``name`` field.
:param name_localizations: Optional[Dict[Union[str, Locale], str]]
:return: A callable response.
:rtype: Callable[..., Any]
"""
Expand All @@ -684,6 +733,7 @@ def decorator(coro: Coroutine) -> Callable[..., Any]:
name=name,
scope=scope,
default_permission=default_permission,
name_localizations=name_localizations,
)
self.__check_command(ApplicationCommand(**commands[0]), coro)

Expand All @@ -706,6 +756,7 @@ def user_command(
name: str,
scope: Optional[Union[int, Guild, List[int], List[Guild]]] = MISSING,
default_permission: Optional[bool] = MISSING,
name_localizations: Optional[Dict[Union[str, Locale], Any]] = MISSING,
) -> Callable[..., Any]:
"""
A decorator for registering a user context menu to the Discord API,
Expand All @@ -729,6 +780,8 @@ async def context_menu_name(ctx):
:type scope: Optional[Union[int, Guild, List[int], List[Guild]]]
:param default_permission?: The default permission of accessibility for the application command. Defaults to ``True``.
:type default_permission: Optional[bool]
:param name_localizations?: The dictionary of localization for the ``name`` field. This enforces the same restrictions as the ``name`` field.
:param name_localizations: Optional[Dict[Union[str, Locale], str]]
:return: A callable response.
:rtype: Callable[..., Any]
"""
Expand All @@ -740,6 +793,7 @@ def decorator(coro: Coroutine) -> Callable[..., Any]:
name=name,
scope=scope,
default_permission=default_permission,
name_localizations=name_localizations,
)

self.__check_command(ApplicationCommand(**commands[0]), coro)
Expand Down Expand Up @@ -1207,11 +1261,11 @@ def __new__(cls, client: Client, *args, **kwargs) -> "Extension":
def teardown(self):
for event, funcs in self._listeners.items():
for func in funcs:
self.client._websocket.dispatch.events[event].remove(func)
self.client._websocket._dispatch.events[event].remove(func)

for cmd, funcs in self._commands.items():
for func in funcs:
self.client._websocket.dispatch.events[cmd].remove(func)
self.client._websocket._dispatch.events[cmd].remove(func)

clean_cmd_names = [cmd[7:] for cmd in self._commands.keys()]
cmds = filter(
Expand Down
6 changes: 5 additions & 1 deletion interactions/client/bot.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ from ..api.models.guild import Guild
from ..api.models.misc import MISSING, Snowflake
from ..api.models.presence import ClientPresence
from ..api.models.team import Application
from .enums import ApplicationCommandType
from .enums import ApplicationCommandType, Locale
from .models.command import ApplicationCommand, Option
from .models.component import Button, Modal, SelectMenu

Expand Down Expand Up @@ -66,20 +66,24 @@ class Client:
scope: Optional[Union[int, Guild, List[int], List[Guild]]] = MISSING,
options: Optional[List[Option]] = MISSING,
default_permission: Optional[bool] = MISSING,
name_localizations: Optional[Dict[Union[str, Locale], str]] = MISSING,
description_localizations: Optional[Dict[Union[str, Locale], str]] = MISSING,
) -> Callable[..., Any]: ...
def message_command(
self,
*,
name: str,
scope: Optional[Union[int, Guild, List[int], List[Guild]]] = MISSING,
default_permission: Optional[bool] = MISSING,
name_localizations: Optional[Dict[Union[str, Locale], str]] = MISSING,
) -> Callable[..., Any]: ...
def user_command(
self,
*,
name: str,
scope: Optional[Union[int, Guild, List[int], List[Guild]]] = MISSING,
default_permission: Optional[bool] = MISSING,
name_localizations: Optional[Dict[Union[str, Locale], str]] = MISSING,
) -> Callable[..., Any]: ...
def component(self, component: Union[Button, SelectMenu]) -> Callable[..., Any]: ...
def autocomplete(
Expand Down