[feat] Add data deletion task and command for verification
This commit is contained in:
parent
f3b7fba57e
commit
9d5ad9784e
6 changed files with 245 additions and 43 deletions
|
@ -1,6 +1,14 @@
|
||||||
default:
|
precommit:
|
||||||
|
stage: test
|
||||||
before_script:
|
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:
|
build:
|
||||||
stage: build
|
stage: build
|
||||||
|
|
|
@ -17,12 +17,16 @@ FROM python:3.12-slim-bookworm as runtime
|
||||||
|
|
||||||
WORKDIR /app
|
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 && \
|
RUN apt-get update && \
|
||||||
apt-get install -y \
|
apt-get install -y \
|
||||||
libjpeg-dev \
|
libjpeg-dev \
|
||||||
libopenjp2-7-dev \
|
libopenjp2-7-dev \
|
||||||
libgl-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 \
|
ENV VIRTUAL_ENV=/app/.venv \
|
||||||
PATH="/app/.venv/bin:$PATH"
|
PATH="/app/.venv/bin:$PATH"
|
||||||
|
|
|
@ -1,7 +1,31 @@
|
||||||
"""JARVIS task mixin."""
|
"""JARVIS task mixin."""
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
import pytz
|
||||||
from aiohttp import ClientSession
|
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.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:
|
class TaskMixin:
|
||||||
|
@ -37,3 +61,64 @@ class TaskMixin:
|
||||||
@Task.create(IntervalTrigger(minutes=30))
|
@Task.create(IntervalTrigger(minutes=30))
|
||||||
async def _update_currencies(self) -> None:
|
async def _update_currencies(self) -> None:
|
||||||
await self.erapi.update_async()
|
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
105
jarvis/cogs/core/data.py
Normal 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)
|
|
@ -41,29 +41,6 @@ from jarvis.utils import build_embed
|
||||||
|
|
||||||
JARVIS_LOGO = Image.open("jarvis_small.png").convert("RGBA")
|
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):
|
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")
|
@bot.subcommand(sub_cmd_name="status", sub_cmd_description="Retrieve JARVIS status")
|
||||||
@cooldown(bucket=Buckets.CHANNEL, rate=1, interval=30)
|
@cooldown(bucket=Buckets.CHANNEL, rate=1, interval=30)
|
||||||
async def _status(self, ctx: SlashContext) -> None:
|
async def _status(self, ctx: SlashContext) -> None:
|
||||||
|
|
|
@ -1,10 +1,15 @@
|
||||||
"""JARVIS Utility Functions."""
|
"""JARVIS Utility Functions."""
|
||||||
|
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from pkgutil import iter_modules
|
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.embed import Embed, EmbedField
|
||||||
from interactions.models.discord.guild import AuditLogEntry
|
from interactions.models.discord.guild import AuditLogEntry
|
||||||
from interactions.models.discord.user import Member
|
from interactions.models.discord.user import Member
|
||||||
|
from jarvis_core.db import models
|
||||||
|
|
||||||
from jarvis.branding import PRIMARY_COLOR
|
from jarvis.branding import PRIMARY_COLOR
|
||||||
|
|
||||||
|
@ -66,3 +71,36 @@ def get_extensions(path: str) -> list:
|
||||||
"""Get JARVIS cogs."""
|
"""Get JARVIS cogs."""
|
||||||
vals = [x.name for x in iter_modules(path)]
|
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()
|
||||||
|
|
Loading…
Add table
Reference in a new issue