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

[V3] ctx.send modifications and other output sanitization #1942

Merged
merged 6 commits into from
Aug 24, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ redbot/core/utils/data_converter.py @mikeshardmind
redbot/core/utils/antispam.py @mikeshardmind
redbot/core/utils/tunnel.py @mikeshardmind
redbot/core/utils/caching.py @mikeshardmind
redbot/core/utils/common_filters.py @mikeshardmind

# Cogs
redbot/cogs/admin/* @tekulvw
Expand Down
8 changes: 7 additions & 1 deletion docs/framework_utils.rst
Original file line number Diff line number Diff line change
Expand Up @@ -44,4 +44,10 @@ Tunnel
======

.. automodule:: redbot.core.utils.tunnel
:members: Tunnel
:members: Tunnel

Common Filters
==============

.. automodule:: redbot.core.utils.common_filters
:members:
9 changes: 7 additions & 2 deletions redbot/cogs/mod/mod.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
from redbot.core.utils.mod import is_mod_or_superior, is_allowed_by_hierarchy, get_audit_reason
from .log import log

from redbot.core.utils.common_filters import filter_invites

_ = Translator("Mod", __file__)


Expand Down Expand Up @@ -1321,9 +1323,11 @@ async def userinfo(self, ctx, *, user: discord.Member = None):
if roles is not None:
data.add_field(name=_("Roles"), value=roles, inline=False)
if names:
data.add_field(name=_("Previous Names"), value=", ".join(names), inline=False)
val = filter_invites(", ".join(names))
data.add_field(name=_("Previous Names"), value=val, inline=False)
if nicks:
data.add_field(name=_("Previous Nicknames"), value=", ".join(nicks), inline=False)
val = filter_invites(", ".join(nicks))
data.add_field(name=_("Previous Nicknames"), value=val, inline=False)
if voice_state and voice_state.channel:
data.add_field(
name=_("Current voice channel"),
Expand All @@ -1334,6 +1338,7 @@ async def userinfo(self, ctx, *, user: discord.Member = None):

name = str(user)
name = " ~ ".join((name, user.nick)) if user.nick else name
name = filter_invites(name)

if user.avatar:
avatar = user.avatar_url
Expand Down
34 changes: 34 additions & 0 deletions redbot/core/bot.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
from .rpc import RPCMixin
from .help_formatter import Help, help as help_
from .sentry import SentryManager
from .utils import common_filters


def _is_submodule(parent, child):
Expand Down Expand Up @@ -292,6 +293,39 @@ def unload_extension(self, name):
if pkg_name.startswith("redbot.cogs."):
del sys.modules["redbot.cogs"].__dict__[name]

async def send_filtered(
destination: discord.abc.Messageable,
filter_mass_mentions=True,
filter_invite_links=True,
filter_all_links=False,
**kwargs,
):
"""
This is a convienience wrapper around

discord.abc.Messageable.send

It takes the destination you'd like to send to, which filters to apply
(defaults on mass mentions, and invite links) and any other parameters
normally accepted by destination.send

This should realistically only be used for responding using user provided
input. (unfortunately, including usernames)
Manually crafted messages which dont take any user input have no need of this
"""

content = kwargs.pop("content", None)

if content:
if filter_mass_mentions:
content = common_filters.filter_mass_mentions(content)
if filter_invite_links:
content = common_filters.filter_invites(content)
if filter_all_links:
content = common_filters.filter_urls(content)

await destination.send(content=content, **kwargs)

def add_cog(self, cog):
for attr in dir(cog):
_attr = getattr(cog, attr)
Expand Down
38 changes: 37 additions & 1 deletion redbot/core/commands/context.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from discord.ext import commands

from redbot.core.utils.chat_formatting import box

from redbot.core.utils import common_filters

TICK = "\N{WHITE HEAVY CHECK MARK}"

Expand All @@ -20,6 +20,42 @@ class Context(commands.Context):
This class inherits from `discord.ext.commands.Context`.
"""

async def send(self, content=None, **kwargs):
"""Sends a message to the destination with the content given.

This acts the same as `discord.ext.commands.Context.send`, with
one added keyword argument as detailed below in *Other Parameters*.

Parameters
----------
content : str
The content of the message to send.

Other Parameters
----------------
filter : Callable[`str`] -> `str`
A function which is used to sanitize the ``content`` before
it is sent. Defaults to
:func:`~redbot.core.utils.common_filters.filter_mass_mentions`.
This must take a single `str` as an argument, and return
the sanitized `str`.
\*\*kwargs
See `discord.ext.commands.Context.send`.

Returns
-------
discord.Message
The message that was sent.

"""

_filter = kwargs.pop("filter", common_filters.filter_mass_mentions)

if _filter and content:
content = _filter(str(content))

return await super().send(content=content, **kwargs)

async def send_help(self) -> List[discord.Message]:
"""Send the command help message.

Expand Down
7 changes: 6 additions & 1 deletion redbot/core/modlog.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
from redbot.core import Config
from redbot.core.bot import Red

from .utils.common_filters import filter_invites, filter_mass_mentions, filter_urls

__all__ = [
"Case",
"CaseType",
Expand Down Expand Up @@ -141,7 +143,9 @@ async def message_content(self, embed: bool = True):
datetime.fromtimestamp(self.modified_at).strftime("%Y-%m-%d %H:%M:%S")
)

user = "{}#{} ({})\n".format(self.user.name, self.user.discriminator, self.user.id)
user = filter_invites(
"{}#{} ({})\n".format(self.user.name, self.user.discriminator, self.user.id)
) # Invites get rendered even in embeds.
if embed:
emb = discord.Embed(title=title, description=reason)

Expand All @@ -160,6 +164,7 @@ async def message_content(self, embed: bool = True):
emb.timestamp = datetime.fromtimestamp(self.created_at)
return emb
else:
user = filter_mass_mentions(filter_urls(user)) # Further sanitization outside embeds
case_text = ""
case_text += "{}\n".format(title)
case_text += "**User:** {}\n".format(user)
Expand Down
81 changes: 81 additions & 0 deletions redbot/core/utils/common_filters.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import re

__all__ = [
"URL_RE",
"INVITE_URL_RE",
"MASS_MENTION_RE",
"filter_urls",
"filter_invites",
"filter_mass_mentions",
]

# regexes
URL_RE = re.compile(r"(https?|s?ftp)://(\S+)", re.I)

INVITE_URL_RE = re.compile(r"(discord.gg|discordapp.com/invite|discord.me)(\S+)", re.I)

MASS_MENTION_RE = re.compile(r"(@)(?=everyone|here)") # This only matches the @ for sanitizing


# convenience wrappers
def filter_urls(to_filter: str) -> str:
"""Get a string with URLs sanitized.

This will match any URLs starting with these protocols:

- ``http://``
- ``https://``
- ``ftp://``
- ``sftp://``

Parameters
----------
to_filter : str
The string to filter.

Returns
-------
str
The sanitized string.

"""
return URL_RE.sub("[SANITIZED URL]", to_filter)


def filter_invites(to_filter: str) -> str:
"""Get a string with discord invites sanitized.

Will match any discord.gg, discordapp.com/invite, or discord.me
invite URL.

Parameters
----------
to_filter : str
The string to filter.

Returns
-------
str
The sanitized string.

"""
return INVITE_URL_RE.sub("[SANITIZED INVITE]", to_filter)


def filter_mass_mentions(to_filter: str) -> str:
"""Get a string with mass mentions sanitized.

Will match any *here* and/or *everyone* mentions.

Parameters
----------
to_filter : str
The string to filter.

Returns
-------
str
The sanitized string.

"""
return MASS_MENTION_RE.sub("@\u200b", to_filter)
5 changes: 3 additions & 2 deletions redbot/core/utils/tunnel.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import sys
import weakref
from typing import List
from .common_filters import filter_mass_mentions

_instances = weakref.WeakValueDictionary({})

Expand Down Expand Up @@ -70,10 +71,10 @@ def __init__(
self.recipient = recipient
self.last_interaction = datetime.utcnow()

async def react_close(self, *, uid: int, message: str):
async def react_close(self, *, uid: int, message: str = ""):
send_to = self.origin if uid == self.sender.id else self.sender
closer = next(filter(lambda x: x.id == uid, (self.sender, self.recipient)), None)
await send_to.send(message.format(closer=closer))
await send_to.send(filter_mass_mentions(message.format(closer=closer)))

@property
def members(self):
Expand Down