Skip to content

Commit c0b1e50

Browse files
Begin work on a data request API (Cog-Creators#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
1 parent bb1a256 commit c0b1e50

38 files changed

Lines changed: 1763 additions & 224 deletions

docs/framework_commands.rst

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,14 @@ extend functionalities used throughout the bot, as outlined below.
1313

1414
.. autofunction:: redbot.core.commands.group
1515

16+
.. autoclass:: redbot.core.commands.Cog
17+
18+
.. automethod:: format_help_for_context
19+
20+
.. automethod:: red_get_data_for_user
21+
22+
.. automethod:: red_delete_data_for_user
23+
1624
.. autoclass:: redbot.core.commands.Command
1725
:members:
1826
:inherited-members: format_help_for_context

docs/guide_cog_creation.rst

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ Open :code:`__init__.py`. In that file, place the following:
9898
9999
from .mycog import Mycog
100100
101+
101102
def setup(bot):
102103
bot.add_cog(Mycog())
103104
@@ -238,3 +239,20 @@ Not all of these are strict requirements (some are) but are all generally advisa
238239
but a cog which takes actions based on messages should not.
239240

240241
15. Respect settings when treating non command messages as commands.
242+
243+
16. Handle user data responsibly
244+
245+
- Don't do unexpected things with user data.
246+
- Don't expose user data to additional audiences without permission.
247+
- Don't collect data your cogs don't need.
248+
- Don't store data in unexpected locations.
249+
Utilize the cog data path, Config, or if you need something more
250+
prompt the owner to provide it.
251+
252+
17. Utilize the data deletion and statement APIs
253+
254+
- See `redbot.core.commands.Cog.red_delete_data_for_user`
255+
- Make a statement about what data your cogs use with the module level
256+
variable ``__red_end_user_data_statement__``.
257+
This should be a string containing a user friendly explanation of what data
258+
your cog stores and why.

docs/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ Welcome to Red - Discord Bot's documentation!
3131
:caption: User guides:
3232

3333
getting_started
34+
red_core_data_statement
3435

3536
.. toctree::
3637
:maxdepth: 2

docs/red_core_data_statement.rst

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
.. Red Core Data Statement
2+
3+
=====================
4+
Red and End User Data
5+
=====================
6+
7+
Notes for everyone
8+
******************
9+
10+
What data Red collects
11+
----------------------
12+
13+
Red and the cogs included with it collect some amount of data
14+
about users for the bot's normal operations.
15+
16+
In particular the bot will keep track of a short history of usernames/nicknames
17+
which actions refer to your Discord account (such as creating a playlist)
18+
as well as the content of specific messages used directly as commands for the bot
19+
(such as reports sent to servers).
20+
21+
By default, Red will not collect any more data than it needs, and will not use it
22+
for anything other than the portion of the Red's functionality that necessitated it.
23+
24+
3rd party extensions may store additional data beyond what Red does by default.
25+
You can use the command ``[p]mydata 3rdparty``
26+
to view statements about how extensions use your data made by the authors of
27+
the specific 3rd party extensions an instance of Red has installed.
28+
29+
How can I delete data Red has about me?
30+
---------------------------------------
31+
32+
The command ``[p]mydata forgetme`` provides a way for users to remove
33+
large portions of their own data from the bot. This command will not
34+
remove operational data, such as a record that your
35+
Discord account was the target of a moderation action.
36+
37+
3rd party extensions to Red are able to delete data when this command
38+
is used as well, but this is something each extension must implement.
39+
If a loaded extension does not implement this, the user will be informed.
40+
41+
Additional Notes for Bot Owners and Hosts
42+
*****************************************
43+
44+
How to comply with a request from Discord Trust & Safety
45+
--------------------------------------------------------
46+
47+
There are a handful of these available to bot owners in the command group
48+
``[p]mydata ownermanagement``.
49+
50+
The most pertinent one if asked to delete data by a member of Trust & Safety
51+
is
52+
53+
``[p]mydata ownermanagement processdiscordrequest``
54+
55+
This will cause the bot to get rid of or disassociate all data
56+
from the specified user ID.
57+
58+
.. warning::
59+
60+
You should not use this unless
61+
Discord has specifically requested this with regard to a deleted user.
62+
This will remove the user from various anti-abuse measures.
63+
If you are processing a manual request from a user, read the next section
64+
65+
66+
How to process deletion requests from users
67+
-------------------------------------------
68+
69+
You can point users to the command ``[p]mydata forgetme`` as a first step.
70+
71+
If users cannot use that for some reason, the command
72+
73+
``[p]mydata ownermanagement deleteforuser``
74+
75+
exists as a way to handle this as if the user had done it themselves.
76+
77+
Be careful about using the other owner level deletion options on behalf of users,
78+
as this may also result in losing operational data such as data used to prevent spam.
79+
80+
What owners and hosts are responsible for
81+
-----------------------------------------
82+
83+
Owners and hosts must comply both with Discord's terms of service and any applicable laws.
84+
Owners and hosts are responsible for all actions their bot takes.
85+
86+
We cannot give specific guidance on this, but recommend that if there are any issues
87+
you be forthright with users, own up to any mistakes, and do your best to handle it.

redbot/cogs/admin/admin.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,10 @@ def __init__(self):
8787
async def cog_before_invoke(self, ctx: commands.Context):
8888
await self._ready.wait()
8989

90+
async def red_delete_data_for_user(self, **kwargs):
91+
""" Nothing to delete """
92+
return
93+
9094
async def handle_migrations(self):
9195

9296
lock = self.config.get_guilds_lock()

redbot/cogs/alias/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,5 @@
44

55
async def setup(bot: Red):
66
cog = Alias(bot)
7-
await cog.initialize()
87
bot.add_cog(cog)
8+
cog.sync_init()

redbot/cogs/alias/alias.py

Lines changed: 98 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1+
import asyncio
2+
import logging
13
from copy import copy
24
from re import search
35
from string import Formatter
4-
from typing import Dict, List
6+
from typing import Dict, List, Literal
57

68
import discord
79
from redbot.core import Config, commands, checks
@@ -14,6 +16,8 @@
1416

1517
_ = Translator("Alias", __file__)
1618

19+
log = logging.getLogger("red.cogs.alias")
20+
1721

1822
class _TrackingFormatter(Formatter):
1923
def __init__(self):
@@ -38,24 +42,107 @@ class Alias(commands.Cog):
3842
and append them to the stored alias.
3943
"""
4044

41-
default_global_settings: Dict[str, list] = {"entries": []}
42-
43-
default_guild_settings: Dict[str, list] = {"entries": []} # Going to be a list of dicts
44-
4545
def __init__(self, bot: Red):
4646
super().__init__()
4747
self.bot = bot
4848
self.config = Config.get_conf(self, 8927348724)
4949

50-
self.config.register_global(**self.default_global_settings)
51-
self.config.register_guild(**self.default_guild_settings)
50+
self.config.register_global(entries=[], handled_string_creator=False)
51+
self.config.register_guild(entries=[])
5252
self._aliases: AliasCache = AliasCache(config=self.config, cache_enabled=True)
53+
self._ready_event = asyncio.Event()
54+
55+
async def red_delete_data_for_user(
56+
self,
57+
*,
58+
requester: Literal["discord_deleted_user", "owner", "user", "user_strict"],
59+
user_id: int,
60+
):
61+
if requester != "discord_deleted_user":
62+
return
63+
64+
await self._ready_event.wait()
65+
await self._aliases.anonymize_aliases(user_id)
66+
67+
async def cog_before_invoke(self, ctx):
68+
await self._ready_event.wait()
69+
70+
async def _maybe_handle_string_keys(self):
71+
# This isn't a normal schema migration because it's being added
72+
# after the fact for GH-3788
73+
if await self.config.handled_string_creator():
74+
return
75+
76+
async with self.config.entries() as alias_list:
77+
bad_aliases = []
78+
for a in alias_list:
79+
for keyname in ("creator", "guild"):
80+
if isinstance((val := a.get(keyname)), str):
81+
try:
82+
a[keyname] = int(val)
83+
except ValueError:
84+
# Because migrations weren't created as changes were made,
85+
# and the prior form was a string of an ID,
86+
# if this fails, there's nothing to go back to
87+
bad_aliases.append(a)
88+
break
89+
90+
for a in bad_aliases:
91+
alias_list.remove(a)
92+
93+
# if this was using a custom group of (guild_id, aliasname) it would be better but...
94+
all_guild_aliases = await self.config.all_guilds()
95+
96+
for guild_id, guild_data in all_guild_aliases.items():
97+
98+
to_set = []
99+
modified = False
100+
101+
for a in guild_data.get("entries", []):
102+
103+
for keyname in ("creator", "guild"):
104+
if isinstance((val := a.get(keyname)), str):
105+
try:
106+
a[keyname] = int(val)
107+
except ValueError:
108+
break
109+
finally:
110+
modified = True
111+
else:
112+
to_set.append(a)
113+
114+
if modified:
115+
await self.config.guild_from_id(guild_id).entries.set(to_set)
116+
117+
await asyncio.sleep(0)
118+
# control yielded per loop since this is most likely to happen
119+
# at bot startup, where this is most likely to have a performance
120+
# hit.
121+
122+
await self.config.handled_string_creator.set(True)
123+
124+
def sync_init(self):
125+
t = asyncio.create_task(self._initialize())
126+
127+
def done_callback(fut: asyncio.Future):
128+
try:
129+
t.result()
130+
except Exception as exc:
131+
log.exception("Failed to load alias cog", exc_info=exc)
132+
# Maybe schedule extension unloading with message to owner in future
133+
134+
t.add_done_callback(done_callback)
135+
136+
async def _initialize(self):
137+
""" Should only ever be a task """
138+
139+
await self._maybe_handle_string_keys()
53140

54-
async def initialize(self):
55-
# This can be where we set the cache_enabled attribute later
56141
if not self._aliases._loaded:
57142
await self._aliases.load_aliases()
58143

144+
self._ready_event.set()
145+
59146
def is_command(self, alias_name: str) -> bool:
60147
"""
61148
The logic here is that if this returns true, the name should not be used for an alias
@@ -327,6 +414,8 @@ async def _list_global_alias(self, ctx: commands.Context):
327414
@commands.Cog.listener()
328415
async def on_message_without_command(self, message: discord.Message):
329416

417+
await self._ready_event.wait()
418+
330419
if message.guild is not None:
331420
if await self.bot.cog_disabled_in_guild(self, message.guild):
332421
return

redbot/cogs/alias/alias_entry.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,30 @@ def __init__(self, config: Config, cache_enabled: bool = True):
9090
self._loaded = False
9191
self._aliases: Dict[Optional[int], Dict[str, AliasEntry]] = {None: {}}
9292

93+
async def anonymize_aliases(self, user_id: int):
94+
95+
async with self.config.entries() as global_aliases:
96+
for a in global_aliases:
97+
if a.get("creator", 0) == user_id:
98+
a["creator"] = 0xDE1
99+
if self._cache_enabled:
100+
self._aliases[None][a["name"]] = AliasEntry.from_json(a)
101+
102+
all_guilds = await self.config.all_guilds()
103+
async for guild_id, guild_data in AsyncIter(all_guilds.items(), steps=100):
104+
for a in guild_data["entries"]:
105+
if a.get("creator", 0) == user_id:
106+
break
107+
else:
108+
continue
109+
# basically, don't build a context manager wihout a need.
110+
async with self.config.guild_from_id(guild_id).entries() as entry_list:
111+
for a in entry_list:
112+
if a.get("creator", 0) == user_id:
113+
a["creator"] = 0xDE1
114+
if self._cache_enabled:
115+
self._aliases[guild_id][a["name"]] = AliasEntry.from_json(a)
116+
93117
async def load_aliases(self):
94118
if not self._cache_enabled:
95119
self._loaded = True

redbot/cogs/audio/apis/playlist_wrapper.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
PRAGMA_SET_read_uncommitted,
2828
PRAGMA_SET_temp_store,
2929
PRAGMA_SET_user_version,
30+
HANDLE_DISCORD_DATA_DELETION_QUERY,
3031
)
3132
from ..utils import PlaylistScope
3233
from .api_utils import PlaylistFetchResult
@@ -58,6 +59,8 @@ def __init__(self, bot: Red, config: Config, conn: APSWConnectionWrapper):
5859
self.statement.get_all_with_filter = PLAYLIST_FETCH_ALL_WITH_FILTER
5960
self.statement.get_all_converter = PLAYLIST_FETCH_ALL_CONVERTER
6061

62+
self.statement.drop_user_playlists = HANDLE_DISCORD_DATA_DELETION_QUERY
63+
6164
async def init(self) -> None:
6265
"""Initialize the Playlist table"""
6366
with concurrent.futures.ThreadPoolExecutor(max_workers=1) as executor:
@@ -247,3 +250,11 @@ async def upsert(
247250
"tracks": json.dumps(tracks),
248251
},
249252
)
253+
254+
async def handle_playlist_user_id_deletion(self, user_id: int):
255+
with concurrent.futures.ThreadPoolExecutor(max_workers=1) as executor:
256+
executor.submit(
257+
self.database.cursor().execute,
258+
self.statement.drop_user_playlists,
259+
{"user_id": user_id},
260+
)

0 commit comments

Comments
 (0)