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

@@ -87,6 +87,10 @@ class Admin(commands.Cog):
async def cog_before_invoke(self, ctx: commands.Context):
await self._ready.wait()
async def red_delete_data_for_user(self, **kwargs):
""" Nothing to delete """
return
async def handle_migrations(self):
lock = self.config.get_guilds_lock()

View File

@@ -4,5 +4,5 @@ from redbot.core.bot import Red
async def setup(bot: Red):
cog = Alias(bot)
await cog.initialize()
bot.add_cog(cog)
cog.sync_init()

View File

@@ -1,7 +1,9 @@
import asyncio
import logging
from copy import copy
from re import search
from string import Formatter
from typing import Dict, List
from typing import Dict, List, Literal
import discord
from redbot.core import Config, commands, checks
@@ -14,6 +16,8 @@ from .alias_entry import AliasEntry, AliasCache, ArgParseError
_ = Translator("Alias", __file__)
log = logging.getLogger("red.cogs.alias")
class _TrackingFormatter(Formatter):
def __init__(self):
@@ -38,24 +42,107 @@ class Alias(commands.Cog):
and append them to the stored alias.
"""
default_global_settings: Dict[str, list] = {"entries": []}
default_guild_settings: Dict[str, list] = {"entries": []} # Going to be a list of dicts
def __init__(self, bot: Red):
super().__init__()
self.bot = bot
self.config = Config.get_conf(self, 8927348724)
self.config.register_global(**self.default_global_settings)
self.config.register_guild(**self.default_guild_settings)
self.config.register_global(entries=[], handled_string_creator=False)
self.config.register_guild(entries=[])
self._aliases: AliasCache = AliasCache(config=self.config, cache_enabled=True)
self._ready_event = asyncio.Event()
async def red_delete_data_for_user(
self,
*,
requester: Literal["discord_deleted_user", "owner", "user", "user_strict"],
user_id: int,
):
if requester != "discord_deleted_user":
return
await self._ready_event.wait()
await self._aliases.anonymize_aliases(user_id)
async def cog_before_invoke(self, ctx):
await self._ready_event.wait()
async def _maybe_handle_string_keys(self):
# This isn't a normal schema migration because it's being added
# after the fact for GH-3788
if await self.config.handled_string_creator():
return
async with self.config.entries() as alias_list:
bad_aliases = []
for a in alias_list:
for keyname in ("creator", "guild"):
if isinstance((val := a.get(keyname)), str):
try:
a[keyname] = int(val)
except ValueError:
# Because migrations weren't created as changes were made,
# and the prior form was a string of an ID,
# if this fails, there's nothing to go back to
bad_aliases.append(a)
break
for a in bad_aliases:
alias_list.remove(a)
# if this was using a custom group of (guild_id, aliasname) it would be better but...
all_guild_aliases = await self.config.all_guilds()
for guild_id, guild_data in all_guild_aliases.items():
to_set = []
modified = False
for a in guild_data.get("entries", []):
for keyname in ("creator", "guild"):
if isinstance((val := a.get(keyname)), str):
try:
a[keyname] = int(val)
except ValueError:
break
finally:
modified = True
else:
to_set.append(a)
if modified:
await self.config.guild_from_id(guild_id).entries.set(to_set)
await asyncio.sleep(0)
# control yielded per loop since this is most likely to happen
# at bot startup, where this is most likely to have a performance
# hit.
await self.config.handled_string_creator.set(True)
def sync_init(self):
t = asyncio.create_task(self._initialize())
def done_callback(fut: asyncio.Future):
try:
t.result()
except Exception as exc:
log.exception("Failed to load alias cog", exc_info=exc)
# Maybe schedule extension unloading with message to owner in future
t.add_done_callback(done_callback)
async def _initialize(self):
""" Should only ever be a task """
await self._maybe_handle_string_keys()
async def initialize(self):
# This can be where we set the cache_enabled attribute later
if not self._aliases._loaded:
await self._aliases.load_aliases()
self._ready_event.set()
def is_command(self, alias_name: str) -> bool:
"""
The logic here is that if this returns true, the name should not be used for an alias
@@ -327,6 +414,8 @@ class Alias(commands.Cog):
@commands.Cog.listener()
async def on_message_without_command(self, message: discord.Message):
await self._ready_event.wait()
if message.guild is not None:
if await self.bot.cog_disabled_in_guild(self, message.guild):
return

View File

@@ -90,6 +90,30 @@ class AliasCache:
self._loaded = False
self._aliases: Dict[Optional[int], Dict[str, AliasEntry]] = {None: {}}
async def anonymize_aliases(self, user_id: int):
async with self.config.entries() as global_aliases:
for a in global_aliases:
if a.get("creator", 0) == user_id:
a["creator"] = 0xDE1
if self._cache_enabled:
self._aliases[None][a["name"]] = AliasEntry.from_json(a)
all_guilds = await self.config.all_guilds()
async for guild_id, guild_data in AsyncIter(all_guilds.items(), steps=100):
for a in guild_data["entries"]:
if a.get("creator", 0) == user_id:
break
else:
continue
# basically, don't build a context manager wihout a need.
async with self.config.guild_from_id(guild_id).entries() as entry_list:
for a in entry_list:
if a.get("creator", 0) == user_id:
a["creator"] = 0xDE1
if self._cache_enabled:
self._aliases[guild_id][a["name"]] = AliasEntry.from_json(a)
async def load_aliases(self):
if not self._cache_enabled:
self._loaded = True

View File

@@ -27,6 +27,7 @@ from ..sql_statements import (
PRAGMA_SET_read_uncommitted,
PRAGMA_SET_temp_store,
PRAGMA_SET_user_version,
HANDLE_DISCORD_DATA_DELETION_QUERY,
)
from ..utils import PlaylistScope
from .api_utils import PlaylistFetchResult
@@ -58,6 +59,8 @@ class PlaylistWrapper:
self.statement.get_all_with_filter = PLAYLIST_FETCH_ALL_WITH_FILTER
self.statement.get_all_converter = PLAYLIST_FETCH_ALL_CONVERTER
self.statement.drop_user_playlists = HANDLE_DISCORD_DATA_DELETION_QUERY
async def init(self) -> None:
"""Initialize the Playlist table"""
with concurrent.futures.ThreadPoolExecutor(max_workers=1) as executor:
@@ -247,3 +250,11 @@ class PlaylistWrapper:
"tracks": json.dumps(tracks),
},
)
async def handle_playlist_user_id_deletion(self, user_id: int):
with concurrent.futures.ThreadPoolExecutor(max_workers=1) as executor:
executor.submit(
self.database.cursor().execute,
self.statement.drop_user_playlists,
{"user_id": user_id},
)

View File

@@ -1,5 +1,6 @@
import asyncio
import logging
from typing import Mapping
from typing import Literal, Mapping
from redbot.core import commands
from ..abc import MixinMeta
@@ -19,3 +20,37 @@ class RedEvents(MixinMeta, metaclass=CompositeMetaClass):
self.api_interface.spotify_api.update_token(api_tokens)
elif service_name == "audiodb":
self.api_interface.global_cache_api.update_token(api_tokens)
async def red_delete_data_for_user(
self,
*,
requester: Literal["discord_deleted_user", "owner", "user", "user_strict"],
user_id: int,
):
await self.cog_ready_event.wait()
if requester in ("discord_deleted_user", "owner"):
await self.playlist_api.handle_playlist_user_id_deletion(user_id)
all_equalizers = await self.config.custom("EQUALIZER").all()
collected_for_removal = []
c = 0
for guild_id, guild_equalizers in all_equalizers.items():
c += 1
if not c % 100:
await asyncio.sleep(0)
for preset_name, preset in guild_equalizers.get("eq_presets", {}).items():
c += 1
if not c % 100:
await asyncio.sleep(0)
if preset.get("author", 0) == user_id:
collected_for_removal.append((guild_id, preset_name))
async with self.config.custom("EQUALIZER").all() as all_eqs:
for guild_id, preset_name in collected_for_removal:
all_eqs[str(guild_id)]["eq_presets"][preset_name]["author"] = 0xDE1

View File

@@ -10,6 +10,8 @@ __all__ = [
"PRAGMA_SET_read_uncommitted",
"PRAGMA_FETCH_user_version",
"PRAGMA_SET_user_version",
# Data Deletion statement
"HANDLE_DISCORD_DATA_DELETION_QUERY",
# Playlist table statements
"PLAYLIST_CREATE_TABLE",
"PLAYLIST_DELETE",
@@ -82,6 +84,33 @@ PRAGMA_SET_user_version: Final[
pragma user_version=3;
"""
# Data Deletion
# This is intentionally 2 seperate transactions due to concerns
# Draper had. This should prevent it from being a large issue,
# as this is no different than triggering a bulk deletion now.
HANDLE_DISCORD_DATA_DELETION_QUERY: Final[
str
] = """
BEGIN TRANSACTION;
UPDATE playlists
SET deleted = true
WHERE scope_id = :user_id ;
UPDATE playlists
SET author_id = 0xde1
WHERE author_id = :user_id ;
COMMIT TRANSACTION;
BEGIN TRANSACTION;
DELETE FROM PLAYLISTS
WHERE deleted=true;
COMMIT TRANSACTION;
"""
# Playlist table statements
PLAYLIST_CREATE_TABLE: Final[
str

View File

@@ -134,3 +134,7 @@ class Bank(commands.Cog):
)
# ENDSECTION
async def red_delete_data_for_user(self, **kwargs):
""" Nothing to delete """
return

View File

@@ -25,6 +25,10 @@ class Cleanup(commands.Cog):
super().__init__()
self.bot = bot
async def red_delete_data_for_user(self, **kwargs):
""" Nothing to delete """
return
@staticmethod
async def check_100_plus(ctx: commands.Context, number: int) -> bool:
"""

View File

@@ -1,9 +1,10 @@
import asyncio
import re
import random
from datetime import datetime, timedelta
from inspect import Parameter
from collections import OrderedDict
from typing import Iterable, List, Mapping, Tuple, Dict, Set
from typing import Iterable, List, Mapping, Tuple, Dict, Set, Literal
from urllib.parse import quote_plus
import discord
@@ -11,7 +12,7 @@ from fuzzywuzzy import process
from redbot.core import Config, checks, commands
from redbot.core.i18n import Translator, cog_i18n
from redbot.core.utils import menus
from redbot.core.utils import menus, AsyncIter
from redbot.core.utils.chat_formatting import box, pagify, escape, humanize_list
from redbot.core.utils.predicates import MessagePredicate
@@ -40,15 +41,35 @@ class OnCooldown(CCError):
class CommandObj:
def __init__(self, **kwargs):
config = kwargs.get("config")
self.config = kwargs.get("config")
self.bot = kwargs.get("bot")
self.db = config.guild
self.db = self.config.guild
@staticmethod
async def get_commands(config) -> dict:
_commands = await config.commands()
return {k: v for k, v in _commands.items() if _commands[k]}
async def redact_author_ids(self, user_id: int):
all_guilds = await self.config.all_guilds()
for guild_id in all_guilds.keys():
await asyncio.sleep(0)
async with self.config.guild_from_id(guild_id).commands() as all_commands:
async for com_name, com_info in AsyncIter(all_commands.items(), steps=100):
if not com_info:
continue
if com_info.get("author", {}).get("id", 0) == user_id:
com_info["author"]["id"] = 0xDE1
com_info["author"]["name"] = "Deleted User"
if editors := com_info.get("editors", None):
for index, editor_id in enumerate(editors):
if editor_id == user_id:
editors[index] = 0xDE1
async def get_responses(self, ctx):
intro = _(
"Welcome to the interactive random {cc} maker!\n"
@@ -200,6 +221,17 @@ class CustomCommands(commands.Cog):
self.commandobj = CommandObj(config=self.config, bot=self.bot)
self.cooldowns = {}
async def red_delete_data_for_user(
self,
*,
requester: Literal["discord_deleted_user", "owner", "user", "user_strict"],
user_id: int,
):
if requester != "discord_deleted_user":
return
await self.commandobj.redact_author_ids(user_id)
@commands.group(aliases=["cc"])
@commands.guild_only()
async def customcom(self, ctx: commands.Context):
@@ -209,7 +241,7 @@ class CustomCommands(commands.Cog):
@customcom.command(name="raw")
async def cc_raw(self, ctx: commands.Context, command: str.lower):
"""Get the raw response of a custom command, to get the proper markdown.
This is helpful for copy and pasting."""
commands = await self.config.guild(ctx.guild).commands()
if command not in commands:
@@ -472,12 +504,14 @@ class CustomCommands(commands.Cog):
if isinstance(responses, str):
responses = [responses]
author = ctx.guild.get_member(cmd["author"]["id"])
# If the author is still in the server, show their current name
if author:
author = "{} ({})".format(author, cmd["author"]["id"])
_aid = cmd["author"]["id"]
if _aid == 0xDE1:
author = _("Deleted User")
elif member := ctx.guild.get_member(_aid):
author = f"{member} ({_aid})"
else:
author = "{} ({})".format(cmd["author"]["name"], cmd["author"]["id"])
author = f"{cmd['author']['name']} ({_aid})"
_type = _("Random") if len(responses) > 1 else _("Normal")

View File

@@ -91,6 +91,10 @@ class Downloader(commands.Cog):
if self._init_task is not None:
self._init_task.cancel()
async def red_delete_data_for_user(self, **kwargs):
""" Nothing to delete """
return
def create_init_task(self):
def _done_callback(task: asyncio.Task) -> None:
exc = task.exception()

View File

@@ -3,7 +3,7 @@ import logging
import random
from collections import defaultdict, deque, namedtuple
from enum import Enum
from typing import cast, Iterable, Union
from typing import cast, Iterable, Union, Literal
import discord
@@ -11,6 +11,7 @@ from redbot.cogs.bank import is_owner_if_bank_global
from redbot.cogs.mod.converters import RawUserIds
from redbot.core import Config, bank, commands, errors, checks
from redbot.core.i18n import Translator, cog_i18n
from redbot.core.utils import AsyncIter
from redbot.core.utils.chat_formatting import box, humanize_number
from redbot.core.utils.menus import menu, DEFAULT_CONTROLS
@@ -136,7 +137,6 @@ class Economy(commands.Cog):
def __init__(self, bot: Red):
super().__init__()
self.bot = bot
self.file_path = "data/economy/settings.json"
self.config = Config.get_conf(self, 1256844281)
self.config.register_guild(**self.default_guild_settings)
self.config.register_global(**self.default_global_settings)
@@ -145,6 +145,23 @@ class Economy(commands.Cog):
self.config.register_role(**self.default_role_settings)
self.slot_register = defaultdict(dict)
async def red_delete_data_for_user(
self,
*,
requester: Literal["discord_deleted_user", "owner", "user", "user_strict"],
user_id: int,
):
if requester != "discord_deleted_user":
return
await self.config.user_from_id(user_id).clear()
all_members = await self.config.all_members()
async for guild_id, guild_data in AsyncIter(all_members.items(), steps=100):
if user_id in guild_data:
await self.config.member_from_ids(guild_id, user_id).clear()
@guild_only_check()
@commands.group(name="bank")
async def _bank(self, ctx: commands.Context):

View File

@@ -1,10 +1,11 @@
import discord
import re
from typing import Union, Set
from typing import Union, Set, Literal
from redbot.core import checks, Config, modlog, commands
from redbot.core.bot import Red
from redbot.core.i18n import Translator, cog_i18n
from redbot.core.utils import AsyncIter
from redbot.core.utils.chat_formatting import pagify, humanize_list
_ = Translator("Filter", __file__)
@@ -33,6 +34,21 @@ class Filter(commands.Cog):
self.register_task = self.bot.loop.create_task(self.register_filterban())
self.pattern_cache = {}
async def red_delete_data_for_user(
self,
*,
requester: Literal["discord_deleted_user", "owner", "user", "user_strict"],
user_id: int,
):
if requester != "discord_deleted_user":
return
all_members = await self.config.all_members()
async for guild_id, guild_data in AsyncIter(all_members.items(), steps=100):
if user_id in guild_data:
await self.config.member_from_ids(guild_id, user_id).clear()
def cog_unload(self):
self.register_task.cancel()

View File

@@ -75,6 +75,10 @@ class General(commands.Cog):
super().__init__()
self.stopwatches = {}
async def red_delete_data_for_user(self, **kwargs):
""" Nothing to delete """
return
@commands.command()
async def choose(self, ctx, *choices):
"""Choose between multiple options.

View File

@@ -26,6 +26,10 @@ class Image(commands.Cog):
def cog_unload(self):
self.session.detach()
async def red_delete_data_for_user(self, **kwargs):
""" Nothing to delete """
return
async def initialize(self) -> None:
"""Move the API keys from cog stored config to core bot config if they exist."""
imgur_token = await self.config.imgur_client_id()

View File

@@ -3,7 +3,7 @@ import logging
import re
from abc import ABC
from collections import defaultdict
from typing import List, Tuple
from typing import List, Tuple, Literal
import discord
from redbot.core.utils import AsyncIter
@@ -83,6 +83,34 @@ class Mod(
self._ready = asyncio.Event()
async def red_delete_data_for_user(
self,
*,
requester: Literal["discord_deleted_user", "owner", "user", "user_strict"],
user_id: int,
):
if requester != "discord_deleted_user":
return
all_members = await self.config.all_members()
async for guild_id, guild_data in AsyncIter(all_members.items(), steps=100):
if user_id in guild_data:
await self.config.member_from_ids(guild_id, user_id).clear()
await self.config.user_from_id(user_id).clear()
guild_data = await self.config.all_guilds()
async for guild_id, guild_data in AsyncIter(guild_data.items(), steps=100):
if user_id in guild_data["current_tempbans"]:
async with self.config.guild_from_id(guild_id).current_tempbans() as tbs:
try:
tbs.remove(user_id)
except ValueError:
pass
# possible with a context switch between here and getting all guilds
async def initialize(self):
await self._maybe_update_config()
self._ready.set()

View File

@@ -20,6 +20,10 @@ class ModLog(commands.Cog):
super().__init__()
self.bot = bot
async def red_delete_data_for_user(self, **kwargs):
""" Nothing to delete """
return
@commands.group()
@checks.guildowner_or_permissions(administrator=True)
async def modlogset(self, ctx: commands.Context):

View File

@@ -138,12 +138,19 @@ class CogOrCommand(NamedTuple):
# noinspection PyArgumentList
@classmethod
async def convert(cls, ctx: commands.Context, arg: str) -> "CogOrCommand":
cog = ctx.bot.get_cog(arg)
if cog:
return cls(type="COG", name=cog.__class__.__name__, obj=cog)
cmd = ctx.bot.get_command(arg)
if cmd:
return cls(type="COMMAND", name=cmd.qualified_name, obj=cmd)
ret = None
if cog := ctx.bot.get_cog(arg):
ret = cls(type="COG", name=cog.qualified_name, obj=cog)
elif cmd := ctx.bot.get_command(arg):
ret = cls(type="COMMAND", name=cmd.qualified_name, obj=cmd)
if ret:
if isinstance(ret.obj, commands.commands._RuleDropper):
raise commands.BadArgument(
"You cannot apply permission rules to this cog or command."
)
return ret
raise commands.BadArgument(
_(

View File

@@ -2,7 +2,7 @@ import asyncio
import io
import textwrap
from copy import copy
from typing import Union, Optional, Dict, List, Tuple, Any, Iterator, ItemsView, cast
from typing import Union, Optional, Dict, List, Tuple, Any, Iterator, ItemsView, Literal, cast
import discord
import yaml
@@ -10,6 +10,7 @@ from schema import And, Or, Schema, SchemaError, Optional as UseOptional
from redbot.core import checks, commands, config
from redbot.core.bot import Red
from redbot.core.i18n import Translator, cog_i18n
from redbot.core.utils import AsyncIter
from redbot.core.utils.chat_formatting import box
from redbot.core.utils.menus import start_adding_reactions
from redbot.core.utils.predicates import ReactionPredicate, MessagePredicate
@@ -114,6 +115,56 @@ class Permissions(commands.Cog):
self.config.init_custom(COMMAND, 1)
self.config.register_custom(COMMAND)
async def red_delete_data_for_user(
self,
*,
requester: Literal["discord_deleted_user", "owner", "user", "user_strict"],
user_id: int,
):
if requester != "discord_deleted_user":
return
count = 0
_uid = str(user_id)
# The dict as returned here as string keys. Above is for comparison,
# there's a below recast to int where needed for guild ids
for typename, getter in ((COG, self.bot.get_cog), (COMMAND, self.bot.get_command)):
obj_type_rules = await self.config.custom(typename).all()
count += 1
if not count % 100:
await asyncio.sleep(0)
for obj_name, rules_dict in obj_type_rules.items():
count += 1
if not count % 100:
await asyncio.sleep(0)
obj = getter(obj_name)
for guild_id, guild_rules in rules_dict.items():
count += 1
if not count % 100:
await asyncio.sleep(0)
if _uid in guild_rules:
if obj:
# delegate to remove rule here
await self._remove_rule(
CogOrCommand(typename, obj.qualified_name, obj),
user_id,
int(guild_id),
)
else:
grp = self.config.custom(typename, obj_name)
await grp.clear_raw(guild_id, user_id)
async def __permissions_hook(self, ctx: commands.Context) -> Optional[bool]:
"""
Purpose of this hook is to prevent guild owner lockouts of permissions specifically
@@ -345,14 +396,6 @@ class Permissions(commands.Cog):
if not who_or_what:
await ctx.send_help()
return
if isinstance(cog_or_command.obj, commands.commands._AlwaysAvailableCommand):
await ctx.send(
_(
"This command is designated as being always available and "
"cannot be modified by permission rules."
)
)
return
for w in who_or_what:
await self._add_rule(
rule=cast(bool, allow_or_deny),
@@ -388,14 +431,6 @@ class Permissions(commands.Cog):
if not who_or_what:
await ctx.send_help()
return
if isinstance(cog_or_command.obj, commands.commands._AlwaysAvailableCommand):
await ctx.send(
_(
"This command is designated as being always available and "
"cannot be modified by permission rules."
)
)
return
for w in who_or_what:
await self._add_rule(
rule=cast(bool, allow_or_deny),
@@ -473,14 +508,6 @@ class Permissions(commands.Cog):
`<cog_or_command>` is the cog or command to set the default
rule for. This is case sensitive.
"""
if isinstance(cog_or_command.obj, commands.commands._AlwaysAvailableCommand):
await ctx.send(
_(
"This command is designated as being always available and "
"cannot be modified by permission rules."
)
)
return
await self._set_default_rule(
rule=cast(Optional[bool], allow_or_deny),
cog_or_cmd=cog_or_command,
@@ -504,14 +531,6 @@ class Permissions(commands.Cog):
`<cog_or_command>` is the cog or command to set the default
rule for. This is case sensitive.
"""
if isinstance(cog_or_command.obj, commands.commands._AlwaysAvailableCommand):
await ctx.send(
_(
"This command is designated as being always available and "
"cannot be modified by permission rules."
)
)
return
await self._set_default_rule(
rule=cast(Optional[bool], allow_or_deny), cog_or_cmd=cog_or_command, guild_id=GLOBAL
)

View File

@@ -1,6 +1,6 @@
import logging
import asyncio
from typing import Union, List
from typing import Union, List, Literal
from datetime import timedelta
from copy import copy
import contextlib
@@ -60,6 +60,39 @@ class Reports(commands.Cog):
# (guild, ticket#):
# {'tun': Tunnel, 'msgs': List[int]}
async def red_delete_data_for_user(
self,
*,
requester: Literal["discord_deleted_user", "owner", "user", "user_strict"],
user_id: int,
):
if requester != "discord_deleted_user":
return
all_reports = await self.config.custom("REPORT").all()
steps = 0
paths = []
# this doesn't use async iter intentionally due to the nested iterations
for guild_id_str, tickets in all_reports.items():
for ticket_number, ticket in tickets.items():
steps += 1
if not steps % 100:
await asyncio.sleep(0) # yield context
if ticket.get("report", {}).get("user_id", 0) == user_id:
paths.append((guild_id_str, ticket_number))
async with self.config.custom("REPORT").all() as all_reports:
async for guild_id_str, ticket_number in AsyncIter(paths, steps=100):
r = all_reports[guild_id_str][ticket_number]["report"]
r["user_id"] = 0xDE1
# this might include EUD, and a report of a deleted user
# that's been unhandled for long enough for the
# user to be deleted and the bot recieve a request like this...
r["report"] = "[REPORT DELETED DUE TO DISCORD REQUEST]"
@property
def tunnels(self):
return [x["tun"] for x in self.tunnel_store.values()]

View File

@@ -79,6 +79,10 @@ class Streams(commands.Cog):
self._ready_event: asyncio.Event = asyncio.Event()
self._init_task: asyncio.Task = self.bot.loop.create_task(self.initialize())
async def red_delete_data_for_user(self, **kwargs):
""" Nothing to delete """
return
def check_name_or_id(self, data: str) -> bool:
matched = self.yt_cid_pattern.fullmatch(data)
if matched is None:

View File

@@ -3,7 +3,7 @@ import asyncio
import math
import pathlib
from collections import Counter
from typing import List
from typing import List, Literal
import io
import yaml
@@ -13,6 +13,7 @@ from redbot.core import Config, commands, checks
from redbot.cogs.bank import is_owner_if_bank_global
from redbot.core.data_manager import cog_data_path
from redbot.core.i18n import Translator, cog_i18n
from redbot.core.utils import AsyncIter
from redbot.core.utils.chat_formatting import box, pagify, bold
from redbot.core.utils.menus import start_adding_reactions
from redbot.core.utils.predicates import MessagePredicate, ReactionPredicate
@@ -56,6 +57,21 @@ class Trivia(commands.Cog):
self.config.register_member(wins=0, games=0, total_score=0)
async def red_delete_data_for_user(
self,
*,
requester: Literal["discord_deleted_user", "owner", "user", "user_strict"],
user_id: int,
):
if requester != "discord_deleted_user":
return
all_members = await self.config.all_members()
async for guild_id, guild_data in AsyncIter(all_members.items(), steps=100):
if user_id in guild_data:
await self.config.member_from_ids(guild_id, user_id).clear()
@commands.group()
@commands.guild_only()
@checks.mod_or_permissions(administrator=True)

View File

@@ -1,7 +1,8 @@
import asyncio
import contextlib
from collections import namedtuple
from copy import copy
from typing import Union, Optional
from typing import Union, Optional, Literal
import discord
@@ -14,6 +15,7 @@ from redbot.cogs.warnings.helpers import (
from redbot.core import Config, checks, commands, modlog
from redbot.core.bot import Red
from redbot.core.i18n import Translator, cog_i18n
from redbot.core.utils import AsyncIter
from redbot.core.utils.chat_formatting import warning, pagify
from redbot.core.utils.menus import menu, DEFAULT_CONTROLS
@@ -45,6 +47,41 @@ class Warnings(commands.Cog):
self.bot = bot
self.registration_task = self.bot.loop.create_task(self.register_warningtype())
async def red_delete_data_for_user(
self,
*,
requester: Literal["discord_deleted_user", "owner", "user", "user_strict"],
user_id: int,
):
if requester != "discord_deleted_user":
return
all_members = await self.config.all_members()
c = 0
for guild_id, guild_data in all_members.items():
c += 1
if not c % 100:
await asyncio.sleep(0)
if user_id in guild_data:
await self.config.member_from_ids(guild_id, user_id).clear()
for remaining_user, user_warns in guild_data.items():
c += 1
if not c % 100:
await asyncio.sleep(0)
for warn_id, warning in user_warns.get("warnings", {}).items():
c += 1
if not c % 100:
await asyncio.sleep(0)
if warning.get("mod", 0) == user_id:
grp = self.config.member_from_ids(guild_id, remaining_user)
await grp.set_raw("warnings", warn_id, "mod", value=0xDE1)
# We're not utilising modlog yet - no need to register a casetype
@staticmethod
async def register_warningtype():
@@ -489,7 +526,11 @@ class Warnings(commands.Cog):
else:
for key in user_warnings.keys():
mod_id = user_warnings[key]["mod"]
mod = ctx.bot.get_user(mod_id) or _("Unknown Moderator ({})").format(mod_id)
if mod_id == 0xDE1:
mod = _("Deleted Moderator")
else:
bot = ctx.bot
mod = bot.get_user(mod_id) or _("Unknown Moderator ({})").format(mod_id)
msg += _(
"{num_points} point warning {reason_name} issued by {user} for "
"{description}\n"
@@ -519,7 +560,11 @@ class Warnings(commands.Cog):
else:
for key in user_warnings.keys():
mod_id = user_warnings[key]["mod"]
mod = ctx.bot.get_user(mod_id) or _("Unknown Moderator ({})").format(mod_id)
if mod_id == 0xDE1:
mod = _("Deleted Moderator")
else:
bot = ctx.bot
mod = bot.get_user(mod_id) or _("Unknown Moderator ({})").format(mod_id)
msg += _(
"{num_points} point warning {reason_name} issued by {user} for "
"{description}\n"