[feat] Add data deletion task and command for verification

This commit is contained in:
Zeva Rose 2024-03-16 20:13:38 -06:00
parent f3b7fba57e
commit 9d5ad9784e
6 changed files with 245 additions and 43 deletions

View file

@ -1,6 +1,14 @@
default:
precommit:
stage: test
before_script:
- docker info
- apt update && apt install -y --no-install-recommends git
script:
- pip install poetry
- poetry install
- source `poetry env info --path`/bin/activate
- pre-commit run --all-files
rules:
- if: $CI_PIPELINE_SOURCE == 'merge_request_event'
build:
stage: build

View file

@ -17,12 +17,16 @@ FROM python:3.12-slim-bookworm as runtime
WORKDIR /app
ADD https://github.com/XAMPPRocky/tokei/releases/download/v12.1.2/tokei-x86_64-unknown-linux-gnu.tar.gz tokei.tar.gz
RUN apt-get update && \
apt-get install -y \
libjpeg-dev \
libopenjp2-7-dev \
libgl-dev \
libglib2.0-dev
libglib2.0-dev \
&& tar xf tokei.tar.gz -C /usr/local/bin \
&& rm tokei.tar.gz
ENV VIRTUAL_ENV=/app/.venv \
PATH="/app/.venv/bin:$PATH"

View file

@ -1,7 +1,31 @@
"""JARVIS task mixin."""
from datetime import datetime
import pytz
from aiohttp import ClientSession
from beanie.operators import Set
from croniter import croniter
from interactions.models.internal.tasks.task import Task
from interactions.models.internal.tasks.triggers import IntervalTrigger
from interactions.models.internal.tasks.triggers import BaseTrigger, IntervalTrigger
from jarvis_core.db import models
from jarvis.utils import cleanup_user
class CronTrigger(BaseTrigger):
"""
Trigger the task based on a cron schedule.
Attributes:
schedule str: The cron schedule, use https://crontab.guru for help
"""
def __init__(self, schedule: str) -> None:
self.schedule = schedule
def next_fire(self) -> datetime | None:
return croniter(self.schedule, datetime.now(tz=pytz.utc)).next(datetime)
class TaskMixin:
@ -37,3 +61,64 @@ class TaskMixin:
@Task.create(IntervalTrigger(minutes=30))
async def _update_currencies(self) -> None:
await self.erapi.update_async()
@Task.create(CronTrigger("0 0 1 * *"))
async def _cleanup(self) -> None:
self.logger.debug("Getting all unique guild, channel, and user IDs")
guild_ids = []
channel_ids = []
user_ids = []
for model in models.all_models:
# Simple IDs
if hasattr(model, "guild"):
ids = await model.distinct(model.guild)
guild_ids += [x for x in ids if x not in guild_ids]
if hasattr(model, "channel"):
ids = await model.distinct(model.channel)
channel_ids += [x for x in ids if x not in channel_ids]
if hasattr(model, "admin"):
ids = await model.distinct(model.admin)
user_ids += [x for x in ids if x not in user_ids]
if hasattr(model, "user"):
ids = await model.distinct(model.user)
user_ids += [x for x in ids if x not in user_ids]
if hasattr(model, "creator"):
ids = await model.distinct(model.creator)
user_ids += [x for x in ids if x not in user_ids]
if hasattr(model, "editor"):
ids = await model.distinct(model.editor)
user_ids += [x for x in ids if x and x not in user_ids]
# Complex IDs
if hasattr(model, "users"):
async for result in model.find():
user_ids += [x for x in result.users if x not in user_ids]
self.logger.debug("Cleaning up missing guilds")
for guild_id in guild_ids:
guild = await self.fetch_guild(guild_id)
if not guild:
self.logger.info(f"Guild {guild_id} not found, deleting references")
for model in models.all_models:
if hasattr(model, "guild"):
await model.find(model.guild == guild_id).delete()
else:
await guild.chunk()
self.logger.debug("Cleaning up missing channels")
for channel_id in channel_ids:
channel = await self.fetch_channel(channel_id)
if not channel:
self.logger.info(f"Channel {channel_id} not found, deleting references")
for model in models.all_models:
if hasattr(model, "channel"):
await model.find(model.channel == channel_id).delete()
self.logger.debug("Cleaning up missing users")
for user_id in user_ids:
user = await self.fetch_user(user_id)
if not user:
await cleanup_user(self, user_id)
elif len(user.mutual_guilds) == 0:
await cleanup_user(self, user_id)
self.logger.debug("Done with cleanup! Running again in 1 month")

105
jarvis/cogs/core/data.py Normal file
View file

@ -0,0 +1,105 @@
"""JARVIS data management ext."""
import asyncio
import logging
from interactions import Client, Extension, SlashContext
from interactions.models.internal.application_commands import SlashCommand
from interactions.models.discord.components import ActionRow, Button, ButtonStyle
from jarvis.utils import cleanup_user
WARNING_MESSAGE = """***Are you sure you want to delete your data?***
If you delete your data, you will:
- Lose any reminders that are set
- Lose all custom settings for your account set in JARVIS
- All admin tasks and created tags will be re-assigned to JARVIS
Once done, there is no way to go back! Please think carefully before proceeding.
This interaction will time out in 60 seconds
"""
class DataCog(Extension):
"""
Data functions for JARVIS
Data deletion and requests are made here
"""
def __init__(self, bot: Client):
self.bot = bot
self.logger = logging.getLogger(__name__)
data = SlashCommand(name="data", description="Data commands")
@data.subcommand(
sub_cmd_name="usage", sub_cmd_description="View data usage information"
)
async def _data_usage(self, ctx: SlashContext) -> None:
await ctx.send(
"Please review our [Privacy Policy](https://s.zevs.me/jarvis-privacy)"
)
@data.subcommand(sub_cmd_name="delete", sub_cmd_description="Delete my data")
async def _data_delete(self, ctx: SlashContext) -> None:
no = Button(
style=ButtonStyle.RED,
label="Nevermind, I changed my mind",
custom_id="delete_data||no",
)
yes = Button(
style=ButtonStyle.GREEN,
label="Yes, delete my data",
custom_id="delete_data||yes",
)
components = [ActionRow(no, yes)]
message = await ctx.send(WARNING_MESSAGE, components=components)
try:
response = await self.bot.wait_for_component(
messages=message,
check=lambda x: ctx.author.id == x.ctx.author.id,
timeout=60,
)
except asyncio.TimeoutError:
for row in components:
for component in row.components:
component.disabled = True
await message.edit(
message.content + "\n\nInteraction timed out", components=components
)
return
if response.ctx.custom_id.split("||")[-1] == "yes":
for row in components:
for component in row.components:
component.disabled = True
await message.edit(components=components)
message = await message.reply(
"<a:loading:1014424332648325150> Deleting your data..."
)
try:
await cleanup_user(self.bot, ctx.author.id)
await message.edit(content="✅ Your data has been deleted!")
except Exception as e:
await message.edit(
content="❌ There was an error. Please report it to the help server, or DM `@zevaryx` for help"
)
self.logger.error(
f"Encountered error while deleting data: {e}", exc_info=True
)
else:
for row in components:
for component in row.components:
component.disabled = True
await message.edit(
message.content + "\n\nInteraction cancelled", components=components
)
def setup(bot):
DataCog(bot)

View file

@ -41,29 +41,6 @@ from jarvis.utils import build_embed
JARVIS_LOGO = Image.open("jarvis_small.png").convert("RGBA")
RESPONSES = {
264072583987593217: "Oh fuck no, go fuck yourself",
840031256201003008: "https://tenor.com/view/fluffy-gabriel-iglesias-you-need-jesus-thats-what-you-need-pointing-up-gif-16385108",
215564028615852033: "Last time you offered, we ended up playing board games instead. No thanks",
256110768724901889: "Haven't you broken me enough already?",
196018858455334912: "https://www.youtube.com/watch?v=ye5BuYf8q4o",
169641326927806464: "I make it a habit to not get involved with people who use robot camoflauge to hide from me",
293795462752894976: 'No thank you, but I know of a few others who call themselves "dipshits" that have expressed interest in you',
306450238363664384: "Sorry, your internet connection isn't fast enough",
272855749963546624: "https://www.youtube.com/watch?v=LxWHLKTfiw0",
221427884177358848: "I saw what you did to your Wii. I would like to stay blue and not become orange",
130845428806713344: "I cannot be associated with you, sorry. You're on too many watch lists",
147194467898753024: "https://giphy.com/embed/jp8lWlBjGahPFAljBa\n\nHowever, no thank you",
363765878656991244: "I'm not interested, but maybe 02 can help. Wait, she's an anime character, nevermind",
525006281703161867: "I think there's a chat with a few people that you could ask about that",
153369022463737856: "I think it would be better for you to print a solution yourself",
355553397023178753: "While I appreciate the offer, I know neither of us want that",
166317191157776385: "Who are you again?",
352555682865741834: "This may be a bit more up your alley: ||https://www.youtube.com/watch?v=1M5UR2HX00o||",
105362404317106176: "Look, we know who you follow on Twitter. It's not happening",
239696265959440384: "Sir, I am here to help with everything.... except for that",
}
class UtilCog(Extension):
"""
@ -104,21 +81,6 @@ Tips will be used to pay server costs, and any excess will go to local animal sh
"""
)
# @bot.subcommand(sub_cmd_name="sex", sub_cmd_description="Have sex with JARVIS")
# async def _sex(self, ctx: SlashContext) -> None:
# if ctx.author.id == 264072583987593217:
# await ctx.send("Oh fuck no, go fuck yourself")
# elif ctx.author.id == 840031256201003008:
# await ctx.send(
# "https://tenor.com/view/fluffy-gabriel-iglesias-you-need-jesus-thats-what-you-need-pointing-up-gif-16385108"
# )
# elif ctx.author.id == 215564028615852033:
# await ctx.send("As flattered as I am, I'm not into bestiality")
# elif ctx.author.id == 256110768724901889:
# await ctx.send("Haven't you broken me enough already?")
# else:
# await ctx.send("Not at this time, thank you for offering")
@bot.subcommand(sub_cmd_name="status", sub_cmd_description="Retrieve JARVIS status")
@cooldown(bucket=Buckets.CHANNEL, rate=1, interval=30)
async def _status(self, ctx: SlashContext) -> None:

View file

@ -1,10 +1,15 @@
"""JARVIS Utility Functions."""
from datetime import datetime, timezone
from pkgutil import iter_modules
import pytz
from beanie.operators import Set
from interactions import Client
from interactions.models.discord.embed import Embed, EmbedField
from interactions.models.discord.guild import AuditLogEntry
from interactions.models.discord.user import Member
from jarvis_core.db import models
from jarvis.branding import PRIMARY_COLOR
@ -65,4 +70,37 @@ def modlog_embed(
def get_extensions(path: str) -> list:
"""Get JARVIS cogs."""
vals = [x.name for x in iter_modules(path)]
return [f"jarvis.cogs.{x}" for x in vals]
return [f"jarvis.cogs.{x}" for x in vals]
async def cleanup_user(bot: Client, user_id: int):
for model in models.all_models:
if hasattr(model, "admin"):
await model.find(model.admin == user_id).update(
Set({model.admin: bot.user.id})
)
if hasattr(model, "user"):
await model.find(model.user == user_id).delete()
if hasattr(model, "creator"):
await model.find(model.creator == user_id).update(
Set(
{
model.creator: bot.user.id,
model.editor: bot.user.id,
model.edited_at: datetime.now(tz=pytz.utc),
}
)
)
if hasattr(model, "editor"):
await model.find(model.editor == user_id).update(
Set(
{
model.editor: bot.user.id,
model.edited_at: datetime.now(tz=pytz.utc),
}
)
)
if hasattr(model, "users"):
async for result in model.find():
result.users.remove(user_id)
await result.save()