mirror of
https://github.com/Cog-Creators/Red-DiscordBot.git
synced 2025-11-20 09:56:05 -05:00
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:
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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},
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -134,3 +134,7 @@ class Bank(commands.Cog):
|
||||
)
|
||||
|
||||
# ENDSECTION
|
||||
|
||||
async def red_delete_data_for_user(self, **kwargs):
|
||||
""" Nothing to delete """
|
||||
return
|
||||
|
||||
@@ -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:
|
||||
"""
|
||||
|
||||
@@ -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")
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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(
|
||||
_(
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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()]
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user