Skip to content
This repository has been archived by the owner on Mar 13, 2023. It is now read-only.

Commit

Permalink
NAFF 2.1.0
Browse files Browse the repository at this point in the history
NAFF 2.1.0
  • Loading branch information
LordOfPolls authored Jan 6, 2023
2 parents ba0163f + 829f17a commit 03ab96e
Show file tree
Hide file tree
Showing 15 changed files with 866 additions and 728 deletions.
6 changes: 3 additions & 3 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,9 @@ repos:
hooks:
- id: flake8
additional_dependencies:
- flake8-annotations~=2.0
- flake8-bandit~=3.0
- flake8-docstrings~=1.5
- flake8-annotations
- flake8-bandit
- flake8-docstrings
- flake8-bugbear
- flake8-comprehensions
- flake8-quotes
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
[![PyPI](https://img.shields.io/pypi/v/naff)](https://pypi.org/project/naff/)
[![Downloads](https://static.pepy.tech/personalized-badge/dis-snek?period=total&units=abbreviation&left_color=grey&right_color=green&left_text=pip%20installs)](https://pepy.tech/project/dis-snek)
[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)
[![black-formatted](https://img.shields.io/github/workflow/status/NAFTeam/NAFF/black-action/master?label=Black%20Format&logo=github)](https://github.com/NAFTeam/NAFF/actions/workflows/black.yml)
[![CodeQL](https://img.shields.io/github/workflow/status/NAFTeam/NAFF/CodeQL/master?label=CodeQL&logo=Github)](https://github.com/NAFTeam/NAFF/actions/workflows/codeql-analysis.yml)
[![pre-commit](https://img.shields.io/github/actions/workflow/status/NAFTeam/NAFF/precommit.yml?branch=master&label=pre-commit&logo=github)](https://github.com/NAFTeam/NAFF/actions/workflows/precommit.yml)
[![CodeQL](https://img.shields.io/github/actions/workflow/status/NAFTeam/NAFF/codeql-analysis.yml?branch=master&label=CodeQL&logo=Github)](https://github.com/NAFTeam/NAFF/actions/workflows/codeql-analysis.yml)
[![Discord](https://img.shields.io/discord/870046872864165888?color=%235865F2&label=Server&logo=discord&logoColor=%235865F2)](https://discord.gg/hpfNhH8BsY)
[![Documentation Status](https://readthedocs.org/projects/naff-docs/badge/?version=latest)](https://naff-docs.readthedocs.io/en/latest/?version=latest)

Expand Down
8 changes: 4 additions & 4 deletions naff/api/events/processors/member_events.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ class MemberEvents(EventMixinTemplate):
async def _on_raw_guild_member_add(self, event: "RawGatewayEvent") -> None:
g_id = event.data.pop("guild_id")
member = self.cache.place_member_data(g_id, event.data)
guild = self.cache.get_guild(g_id)
guild.member_count += 1
if guild := self.cache.get_guild(g_id):
guild.member_count += 1
self.dispatch(events.MemberAdd(g_id, member))

@Processor.define()
Expand All @@ -27,8 +27,8 @@ async def _on_raw_guild_member_remove(self, event: "RawGatewayEvent") -> None:
member = self.cache.get_member(g_id, user.id)

self.cache.delete_member(g_id, user.id)
guild = self.cache.get_guild(g_id)
guild.member_count -= 1
if guild := self.cache.get_guild(g_id):
guild.member_count -= 1

self.dispatch(events.MemberRemove(g_id, member or user))

Expand Down
13 changes: 5 additions & 8 deletions naff/api/events/processors/message_events.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,11 @@ async def _on_raw_message_create(self, event: "RawGatewayEvent") -> None:
if not msg._guild_id and event.data.get("guild_id"):
msg._guild_id = event.data["guild_id"]

if not msg.author:
# sometimes discord will only send an author ID, not the author. this catches that
await self.cache.fetch_channel(to_snowflake(msg._channel_id)) if not msg.channel else msg.channel
if msg._guild_id:
await self.cache.fetch_guild(msg._guild_id) if not msg.guild else msg.guild
await self.cache.fetch_member(msg._guild_id, msg._author_id)
else:
await self.cache.fetch_user(to_snowflake(msg._author_id))
if msg._guild_id and not msg.guild:
await self.cache.fetch_guild(msg._guild_id)

if not msg.channel:
await self.cache.fetch_channel(to_snowflake(msg._channel_id))

self.dispatch(events.MessageCreate(msg))

Expand Down
15 changes: 11 additions & 4 deletions naff/api/http/http_requests/guild.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import TYPE_CHECKING, cast, Mapping, Any
from typing import TYPE_CHECKING, List, cast, Mapping, Any

import discord_typings

Expand Down Expand Up @@ -371,22 +371,29 @@ async def create_guild_role(
return cast(discord_typings.RoleData, result)

async def modify_guild_role_positions(
self, guild_id: "Snowflake_Type", role_id: "Snowflake_Type", position: int, reason: str | None = None
self,
guild_id: "Snowflake_Type",
position_changes: List[dict["Snowflake_Type", int]],
reason: str | None = None,
) -> list[discord_typings.RoleData]:
"""
Modify the position of a role in the guild.
Args:
guild_id: The ID of the guild
role_id: The ID of the role to move
position_changes: A list of dicts representing the roles to move and their new positions
``{"id": role_id, "position": new_position}``
position: The new position of this role in the hierarchy
reason: The reason for this action
Returns:
List of guild roles
"""
payload: PAYLOAD_TYPE = {"id": int(role_id), "position": position}
payload: PAYLOAD_TYPE = [
{"id": int(role["id"]), "position": int(role["position"])} for role in position_changes
]
result = await self.request(Route("PATCH", f"/guilds/{int(guild_id)}/roles"), payload=payload, reason=reason)
return cast(list[discord_typings.RoleData], result)

Expand Down
186 changes: 112 additions & 74 deletions naff/client/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@

import naff.api.events as events
import naff.client.const as constants
from naff.api.events import MessageCreate, RawGatewayEvent, processors, Component, BaseEvent
from naff.api.events import BaseEvent, Component, RawGatewayEvent, processors, MessageCreate
from naff.api.gateway.gateway import GatewayClient
from naff.api.gateway.state import ConnectionState
from naff.api.http.http_client import HTTPClient
Expand Down Expand Up @@ -1752,90 +1752,128 @@ async def _dispatch_interaction(self, event: RawGatewayEvent) -> None:
else:
raise NotImplementedError(f"Unknown Interaction Received: {interaction_data['type']}")

@Listener.create("message_create", is_default_listener=True)
async def _dispatch_prefixed_commands(self, event: MessageCreate) -> None:
@Listener.create("raw_message_create", is_default_listener=True)
async def _dispatch_prefixed_commands(self, event: RawGatewayEvent) -> None:
"""Determine if a prefixed command is being triggered, and dispatch it."""
message = event.message
# don't waste time processing this if there are no prefixed commands
if not self.prefixed_commands:
return

data = event.data

if not message.content:
# many bots will not have the message content intent, and so will not have content
# for most messages. since there's nothing for prefixed commands to work off of,
# we might as well not waste time
if not data.get("content"):
return

if not message.author.bot:
prefixes: str | Iterable[str] = await self.generate_prefixes(self, message)
# webhooks and users labeled with the bot property are bots, and should be ignored
if data.get("webhook_id") or data["author"].get("bot", False):
return

if isinstance(prefixes, str) or prefixes == MENTION_PREFIX:
# its easier to treat everything as if it may be an iterable
# rather than building a special case for this
prefixes = (prefixes,) # type: ignore
# now, we've done the basic filtering out, but everything from here on out relies
# on a proper message object, so now we either hope its already in the cache or wait
# on the processor

prefix_used = None
# first, let's check the cache...
message = self.cache.get_message(int(data["channel_id"]), int(data["id"]))

for prefix in prefixes:
if prefix == MENTION_PREFIX:
if mention := self._mention_reg.search(message.content): # type: ignore
prefix = mention.group()
else:
continue
# this huge if statement basically checks if the message hasn't been fully processed by
# the processor yet, which would mean that these fields aren't fully filled
if message and (
(not message._guild_id and event.data.get("guild_id"))
or (message._guild_id and not message.guild)
or not message.channel
):
message = None

if message.content.startswith(prefix):
prefix_used = prefix
break
# if we didn't get a message, then we know we should wait for the message create event
if not message:
try:
# i think 2 seconds is a very generous timeout limit
event: MessageCreate = await self.wait_for(
MessageCreate, checks=lambda e: int(e.message.id) == int(data["id"]), timeout=2
)
message = event.message
except asyncio.TimeoutError:
return

if prefix_used:
context = await self.get_context(message)
context.prefix = prefix_used

# interestingly enough, we cannot count on ctx.invoke_target
# being correct as its hard to account for newlines and the like
# with the way we get subcommands here
# we'll have to reconstruct it by getting the content_parameters
# then removing the prefix and the parameters from the message
# content
content_parameters = message.content.removeprefix(prefix_used) # type: ignore
command = self # yes, this is a hack

while True:
first_word: str = get_first_word(content_parameters) # type: ignore
if isinstance(command, PrefixedCommand):
new_command = command.subcommands.get(first_word)
else:
new_command = command.prefixed_commands.get(first_word)
if not new_command or not new_command.enabled:
break
# here starts the actual prefixed command parsing part
prefixes: str | Iterable[str] = await self.generate_prefixes(self, message)

command = new_command
content_parameters = content_parameters.removeprefix(first_word).strip()

if command.subcommands and command.hierarchical_checking:
try:
await new_command._can_run(context) # will error out if we can't run this command
except Exception as e:
if new_command.error_callback:
await new_command.error_callback(e, context)
elif new_command.extension and new_command.extension.extension_error:
await new_command.extension.extension_error(e, context)
else:
self.dispatch(events.CommandError(ctx=context, error=e))
return

if not isinstance(command, PrefixedCommand):
command = None

if command and command.enabled:
# yeah, this looks ugly
context.command = command
context.invoke_target = message.content.removeprefix(prefix_used).removesuffix(content_parameters).strip() # type: ignore
context.args = get_args(context.content_parameters)
try:
if self.pre_run_callback:
await self.pre_run_callback(context)
await self._run_prefixed_command(command, context)
if self.post_run_callback:
await self.post_run_callback(context)
except Exception as e:
if isinstance(prefixes, str) or prefixes == MENTION_PREFIX:
# its easier to treat everything as if it may be an iterable
# rather than building a special case for this
prefixes = (prefixes,) # type: ignore

prefix_used = None

for prefix in prefixes:
if prefix == MENTION_PREFIX:
if mention := self._mention_reg.search(message.content): # type: ignore
prefix = mention.group()
else:
continue

if message.content.startswith(prefix):
prefix_used = prefix
break

if not prefix_used:
return

context = await self.get_context(message)
context.prefix = prefix_used

# interestingly enough, we cannot count on ctx.invoke_target
# being correct as its hard to account for newlines and the like
# with the way we get subcommands here
# we'll have to reconstruct it by getting the content_parameters
# then removing the prefix and the parameters from the message
# content
content_parameters = message.content.removeprefix(prefix_used) # type: ignore
command: "Client | PrefixedCommand" = self # yes, this is a hack

while True:
first_word: str = get_first_word(content_parameters) # type: ignore
if isinstance(command, PrefixedCommand):
new_command = command.subcommands.get(first_word)
else:
new_command = command.prefixed_commands.get(first_word)
if not new_command or not new_command.enabled:
break

command = new_command
content_parameters = content_parameters.removeprefix(first_word).strip()

if command.subcommands and command.hierarchical_checking:
try:
await new_command._can_run(context) # will error out if we can't run this command
except Exception as e:
if new_command.error_callback:
await new_command.error_callback(e, context)
elif new_command.extension and new_command.extension.extension_error:
await new_command.extension.extension_error(e, context)
else:
self.dispatch(events.CommandError(ctx=context, error=e))
finally:
self.dispatch(events.CommandCompletion(ctx=context))
return

if not isinstance(command, PrefixedCommand) or not command.enabled:
return

context.command = command
context.invoke_target = message.content.removeprefix(prefix_used).removesuffix(content_parameters).strip() # type: ignore
context.args = get_args(context.content_parameters)
try:
if self.pre_run_callback:
await self.pre_run_callback(context)
await self._run_prefixed_command(command, context)
if self.post_run_callback:
await self.post_run_callback(context)
except Exception as e:
self.dispatch(events.CommandError(ctx=context, error=e))
finally:
self.dispatch(events.CommandCompletion(ctx=context))

@Listener.create("disconnect", is_default_listener=True)
async def _disconnect(self) -> None:
Expand Down
9 changes: 5 additions & 4 deletions naff/ext/debug_extension/debug_exec.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,8 +81,9 @@ async def debug_exec(self, ctx: InteractionContext) -> Optional[Message]:
return await self.handle_exec_result(m_ctx, ret, stdout.getvalue(), body)

async def handle_exec_result(self, ctx: ModalContext, result: Any, value: Any, body: str) -> Optional[Message]:
if len(body) <= 2000:
await ctx.send(f"```py\n{body}```")
# body can be of length 2000 and exceed the limit after formatting
if len(cmd_body := f"```py\n{body}```") <= 2000:
await ctx.send(cmd_body)

else:
paginator = Paginator.create_from_string(self.bot, body, prefix="```py", suffix="```", page_size=4000)
Expand Down Expand Up @@ -123,8 +124,8 @@ async def handle_exec_result(self, ctx: ModalContext, result: Any, value: Any, b
# prevent token leak
result = result.replace(self.bot.http.token, "[REDACTED TOKEN]")

if len(result) <= 2000:
return await ctx.send(f"```py\n{result}```")
if len(cmd_result := f"```py\n{result}```") <= 2000:
return await ctx.send(cmd_result)

else:
paginator = Paginator.create_from_string(self.bot, result, prefix="```py", suffix="```", page_size=4000)
Expand Down
26 changes: 23 additions & 3 deletions naff/models/discord/channel.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@
"GuildForum",
"GuildNewsThread",
"GuildPublicThread",
"GuildForumPost",
"GuildPrivateThread",
"GuildVoice",
"GuildStageVoice",
Expand Down Expand Up @@ -1717,6 +1718,7 @@ async def edit(
topic=topic,
type=channel_type,
default_auto_archive_duration=default_auto_archive_duration,
rate_limit_per_user=rate_limit_per_user,
reason=reason,
**kwargs,
)
Expand Down Expand Up @@ -2645,17 +2647,35 @@ def process_permission_overwrites(
TYPE_DM_CHANNEL = Union[DM, DMGroup]


TYPE_GUILD_CHANNEL = Union[GuildCategory, GuildNews, GuildText, GuildVoice, GuildStageVoice, GuildForum]
TYPE_GUILD_CHANNEL = Union[
GuildCategory,
GuildNews,
GuildText,
GuildVoice,
GuildStageVoice,
GuildForum,
GuildPublicThread,
GuildForumPost,
GuildPrivateThread,
]


TYPE_THREAD_CHANNEL = Union[GuildNewsThread, GuildPublicThread, GuildPrivateThread]
TYPE_THREAD_CHANNEL = Union[GuildNewsThread, GuildPublicThread, GuildForumPost, GuildPrivateThread]


TYPE_VOICE_CHANNEL = Union[GuildVoice, GuildStageVoice]


TYPE_MESSAGEABLE_CHANNEL = Union[
DM, DMGroup, GuildNews, GuildText, GuildPublicThread, GuildPrivateThread, GuildNewsThread, GuildVoice
DM,
DMGroup,
GuildNews,
GuildText,
GuildPublicThread,
GuildForumPost,
GuildPrivateThread,
GuildNewsThread,
GuildVoice,
]


Expand Down
Loading

0 comments on commit 03ab96e

Please sign in to comment.