Begin work on a data request API (#4045)

[Core] Data Deletion And Disclosure APIs

 - Adds a Data Deletion API
   - Deletion comes in a few forms based on who is requesting
   - Deletion must be handled by 3rd party
 - Adds a Data Collection Disclosure Command
   - Provides a dynamically generated statement from 3rd party
   extensions
 - Modifies the always available commands to be cog compatible
   - Also prevents them from being unloaded accidentally
This commit is contained in:
Michael H
2020-08-03 09:09:07 -04:00
committed by GitHub
parent bb1a256295
commit c0b1e50a5f
38 changed files with 1761 additions and 222 deletions

View File

@@ -15,6 +15,7 @@ from .commands import (
GroupMixin as GroupMixin,
command as command,
group as group,
RedUnhandledAPI as RedUnhandledAPI,
RESERVED_COMMAND_NAMES as RESERVED_COMMAND_NAMES,
)
from .context import Context as Context, GuildContext as GuildContext, DMContext as DMContext

View File

@@ -6,19 +6,23 @@ be used instead of those from the `discord.ext.commands` module.
from __future__ import annotations
import inspect
import io
import re
import functools
import weakref
from typing import (
Any,
Awaitable,
Callable,
Dict,
List,
Literal,
Optional,
Tuple,
Union,
MutableMapping,
TYPE_CHECKING,
cast,
)
import discord
@@ -55,6 +59,7 @@ __all__ = [
"command",
"group",
"RESERVED_COMMAND_NAMES",
"RedUnhandledAPI",
]
#: The following names are reserved for various reasons
@@ -66,6 +71,12 @@ _ = Translator("commands.commands", __file__)
DisablerDictType = MutableMapping[discord.Guild, Callable[["Context"], Awaitable[bool]]]
class RedUnhandledAPI(Exception):
""" An exception which can be raised to signal a lack of handling specific APIs """
pass
class CogCommandMixin:
"""A mixin for cogs and commands."""
@@ -731,6 +742,7 @@ class CogGroupMixin:
whether or not the rule was changed as a result of this
call.
:meta private:
"""
cur_rule = self.requires.get_rule(model_id, guild_id=guild_id)
if cur_rule not in (PermState.NORMAL, PermState.ACTIVE_ALLOW, PermState.ACTIVE_DENY):
@@ -809,6 +821,136 @@ class CogMixin(CogGroupMixin, CogCommandMixin):
if doc:
return inspect.cleandoc(translator(doc))
async def red_get_data_for_user(self, *, user_id: int) -> MutableMapping[str, io.BytesIO]:
"""
.. note::
This method is documented provisionally
and may have minor changes made to it.
It is not expected to undergo major changes,
but nothing utilizes this method yet and the inclusion of this method
in documentation in advance is solely to allow cog creators time to prepare.
This should be overridden by all cogs.
Overridden implementations should return a mapping of filenames to io.BytesIO
containing a human-readable version of the data
the cog has about the specified user_id or an empty mapping
if the cog does not have end user data.
The data should be easily understood for what it represents to
most users of age to use Discord.
You may want to include a readme file
which explains specifics about the data.
This method may also be implemented for an extension.
Parameters
----------
user_id: int
Returns
-------
MutableMapping[str, io.BytesIO]
A mapping of filenames to BytesIO objects
suitable to send as a files or as part of an archive to a user.
This may be empty if you don't have data for users.
Raises
------
RedUnhandledAPI
If the method was not overriden,
or an overriden implementation is not handling this
"""
raise RedUnhandledAPI()
async def red_delete_data_for_user(
self,
*,
requester: Literal["discord_deleted_user", "owner", "user", "user_strict"],
user_id: int,
):
"""
This should be overridden by all cogs.
If your cog does not store data, overriding and doing nothing should still
be done to indicate that this has been considered.
.. note::
This may receive other strings in the future without warning
you should safely handle
any string value (log a warning if needed)
as additional requester types may be added
in the future without prior warning.
(see what this method can raise for details)
This method can currently be passed one of these strings:
- ``"discord_deleted_user"``:
The request should be processed as if
Discord has asked for the data removal
This then additionally must treat the
user ID itself as something to be deleted.
The user ID is no longer operational data
as the ID no longer refers to a valid user.
- ``"owner"``:
The request was made by the bot owner.
If removing the data requested by the owner
would be an operational hazard
(such as removing a user id from a blocked user list)
you may elect to inform the user of an alternative way
to remove that ID to ensure the process can not be abused
by users to bypass anti-abuse measures,
but there must remain a way for them to process this request.
- ``"user_strict"``:
The request was made by a user,
the bot settings allow a user to request their own data
be deleted, and the bot is configured to respect this
at the cost of functionality.
Cogs may retain data needed for anti abuse measures
such as IDs and timestamps of interactions,
but should not keep EUD such
as user nicknames if receiving a request of this nature.
- ``"user"``:
The request was made by a user,
the bot settings allow a user to request their own data
be deleted, and the bot is configured to let cogs keep
data needed for operation.
Under this case, you may elect to retain data which is
essential to the functionality of the cog. This case will
only happen if the bot owner has opted into keeping
minimal EUD needed for cog functionality.
Parameters
----------
requester: Literal["discord_deleted_user", "owner", "user", "user_strict"]
See above notes for details about this parameter
user_id: int
The user ID which needs deletion handling
Raises
------
RedUnhandledAPI
If the method was not overriden,
or an overriden implementation is not handling this
"""
raise RedUnhandledAPI()
async def can_run(self, ctx: "Context", **kwargs) -> bool:
"""
This really just exists to allow easy use with other methods using can_run
@@ -826,6 +968,8 @@ class CogMixin(CogGroupMixin, CogCommandMixin):
-------
bool
``True`` if this cog is usable in the given context.
:meta private:
"""
try:
@@ -854,6 +998,7 @@ class CogMixin(CogGroupMixin, CogCommandMixin):
bool
``True`` if this cog is visible in the given context.
:meta private:
"""
return await self.can_run(ctx)
@@ -873,6 +1018,8 @@ class Cog(CogMixin, DPYCog, metaclass=DPYCogMeta):
"""
This does not have identical behavior to
Group.all_commands but should return what you expect
:meta private:
"""
return {cmd.name: cmd for cmd in self.__cog_commands__}
@@ -917,13 +1064,15 @@ def get_command_disabler(guild: discord.Guild) -> Callable[["Context"], Awaitabl
return disabler
# This is intentionally left out of `__all__` as it is not intended for general use
class _AlwaysAvailableCommand(Command):
# The below are intentionally left out of `__all__`
# as they are not intended for general use
class _AlwaysAvailableMixin:
"""
This should be used only for informational commands
This should be used for commands
which should not be disabled or removed
These commands cannot belong to a cog.
These commands cannot belong to any cog except Core (core_commands.py)
to prevent issues with the appearance of certain behavior.
These commands do not respect most forms of checks, and
should only be used with that in mind.
@@ -931,10 +1080,56 @@ class _AlwaysAvailableCommand(Command):
This particular class is not supported for 3rd party use
"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if self.cog is not None:
raise TypeError("This command may not be added to a cog")
async def can_run(self, ctx, *args, **kwargs) -> bool:
return not ctx.author.bot
can_see = can_run
class _RuleDropper(CogCommandMixin):
"""
Objects inheriting from this, be they command or cog,
should not be interfered with operation except by their own rules,
or by global checks which are not tailored for these objects but instead
on global abuse prevention
(such as a check that disallows blocked users and bots from interacting.)
This should not be used by 3rd-party extensions directly for their own objects.
"""
def allow_for(self, model_id: Union[int, str], guild_id: int) -> None:
""" This will do nothing. """
def deny_to(self, model_id: Union[int, str], guild_id: int) -> None:
""" This will do nothing. """
def clear_rule_for(
self, model_id: Union[int, str], guild_id: int
) -> Tuple[PermState, PermState]:
"""
This will do nothing, except return a compatible rule
"""
cur_rule = self.requires.get_rule(model_id, guild_id=guild_id)
return cur_rule, cur_rule
def set_default_rule(self, rule: Optional[bool], guild_id: int) -> None:
""" This will do nothing. """
class _AlwaysAvailableCommand(_AlwaysAvailableMixin, _RuleDropper, Command):
pass
class _AlwaysAvailableGroup(_AlwaysAvailableMixin, _RuleDropper, Group):
pass
class _ForgetMeSpecialCommand(_RuleDropper, Command):
"""
We need special can_run behavior here
"""
async def can_run(self, ctx, *args, **kwargs) -> bool:
return await ctx.bot._config.datarequests.allow_user_requests()
can_see = can_run