feat(bot): ✨ Add "Delete My Activity Data" button to p.me.

This commit is contained in:
Nyeki.py
2024-11-02 23:39:18 -03:00
parent c3c44a57a6
commit ddd439d6d7
7 changed files with 135 additions and 228 deletions
+27 -6
View File
@@ -1,17 +1,38 @@
apiVersion: batch/v1 apiVersion: apps/v1
kind: CronJob kind: Deployment
metadata: metadata:
name: tasks name: tasks
spec: spec:
schedule: "* * * * *" replicas: 1
jobTemplate: selector:
spec: matchLabels:
app: tasks
template: template:
metadata:
labels:
app: tasks
spec: spec:
containers: containers:
- name: tasks - name: flush
image: git.nyeki.dev/payphone/payphone-tasks:latest image: git.nyeki.dev/payphone/payphone-tasks:latest
command: ["uv", "run", "--no-dev", "tasks.py", "flush"] command: ["uv", "run", "--no-dev", "tasks.py", "flush"]
resources:
requests:
memory: "250Mib"
cpu: "1"
limits:
memory: "1Gi"
cpu: "1"
- name: delete-accounts
image: git.nyeki.dev/payphone/payphone-tasks:latest
command: ["uv", "run", "--no-dev", "tasks.py", "delete-accounts"]
resources:
requests:
memory: "250Mib"
cpu: "1"
limits:
memory: "1Gi"
cpu: "1"
restartPolicy: OnFailure restartPolicy: OnFailure
imagePullSecrets: imagePullSecrets:
- name: regcred - name: regcred
+3 -2
View File
@@ -14,8 +14,9 @@ import httpx
from payphone import bot from payphone import bot
from payphone.db import models, utils from payphone.db import models, utils
from payphone.cogs import base
from payphone.core import constants from payphone.core import constants
from payphone.cogs import base
from payphone.cogs.users import views
class Users(base.Cog): class Users(base.Cog):
@@ -511,7 +512,7 @@ class Users(base.Cog):
thumbnail=profile.avatar_url, thumbnail=profile.avatar_url,
) )
await ctx.send(embed=embed) await ctx.send(embed=embed, view=views.MeView(ctx.author.id))
@commands.command(aliases=["l", "leader", "ranking"]) @commands.command(aliases=["l", "leader", "ranking"])
async def leaderboard(self, ctx: commands.Context, board: str = "users") -> None: async def leaderboard(self, ctx: commands.Context, board: str = "users") -> None:
+60
View File
@@ -0,0 +1,60 @@
import discord
from payphone.db import models
class MeView(discord.ui.View):
status: int = 0
def __init__(self, user_id: int) -> None:
"""Initializes the view.
Args:
user_id (int): The ID of the user to
"""
self.user_id = user_id
super().__init__(disable_on_timeout=True)
@discord.ui.button(
label="Delete My Activity Data", style=discord.ButtonStyle.danger
)
async def callback(
self, button: discord.Button, interaction: discord.Interaction
) -> None:
"""Handles the interaction of the button."""
if interaction.user.id != self.user_id:
await interaction.response.send_message(
"❌ You cannot delete other person's account.", ephemeral=True
)
return
if self.status == 0:
self.children[0].label = "Confirm?"
await interaction.response.edit_message(view=self)
self.status += 1
return
await models.User.filter(id=self.user_id).update(is_deleting=True)
self.disable_all_items()
self.children[0].label = "Your account will be deleted"
await interaction.response.send_message(
(
"👋 Your account was scheduled for deletion. It may take up to 24 hours for your "
"account information to be deleted.\n\n"
"What will be deleted:\n"
"- Your messages\n"
"- Your calls\n"
"- Your profiles and user information\n"
"- Your stats\n"
"- Your reports\n\n"
"What won't be deleted:\n"
"- Your bans and ban history"
),
ephemeral=True,
)
await interaction.message.edit(view=self)
@@ -0,0 +1,21 @@
from tortoise import BaseDBAsyncClient
async def upgrade(db: BaseDBAsyncClient) -> str:
return """
DROP INDEX "idx_report_status_d6b157";
DROP INDEX "idx_ban_user_id_37a9cf";
ALTER TABLE "user" ADD "is_deleting" BOOL NOT NULL DEFAULT False;
CREATE INDEX "idx_ban_user_id_37a9cf" ON "ban" ("user_id", "expires_at");;
CREATE INDEX "idx_report_status_d6b157" ON "report" ("status", "priority");;
CREATE INDEX "idx_user_is_dele_2b4cfd" ON "user" ("is_deleting");"""
async def downgrade(db: BaseDBAsyncClient) -> str:
return """
DROP INDEX "idx_report_status_d6b157";
DROP INDEX "idx_user_is_dele_2b4cfd";
DROP INDEX "idx_ban_user_id_37a9cf";
ALTER TABLE "user" DROP COLUMN "is_deleting";
CREATE INDEX "idx_ban_user_id_37a9cf" ON "ban" ("user_id", "expires_at");;
CREATE INDEX "idx_report_status_d6b157" ON "report" ("status", "priority");;"""
+1
View File
@@ -18,6 +18,7 @@ class User(models.Model):
username: str = fields.CharField(max_length=32, db_index=True) username: str = fields.CharField(max_length=32, db_index=True)
is_masked: bool = fields.BooleanField(default=False) is_masked: bool = fields.BooleanField(default=False)
is_moderator: bool = fields.BooleanField(default=False) is_moderator: bool = fields.BooleanField(default=False)
is_deleting: bool = fields.BooleanField(default=False, db_index=True)
found_easter_eggs: int = fields.IntEnumField(EasterEggCommand, default=0) found_easter_eggs: int = fields.IntEnumField(EasterEggCommand, default=0)
profiles: fields.BackwardFKRelation["Profile"] profiles: fields.BackwardFKRelation["Profile"]
call_rss: fields.BackwardFKRelation["CallUserChannelRelationship"] call_rss: fields.BackwardFKRelation["CallUserChannelRelationship"]
-211
View File
@@ -1,211 +0,0 @@
from multiprocessing import managers
from datetime import datetime, timezone
from payphone.core import logging
log = logging.logger
def shard(
shard_id: int,
shard_count: int,
shutdown: managers.ValueProxy[bool],
scheduled_shutdown: managers.ValueProxy[bool],
) -> None:
"""Starts a new shard.
Args:
shard_id (int): The shard's ID.
shard_count (int): The total amount of shards.
shutdown (ValueProxy[bool]): The flag for the main process to shut down all shards.
scheduled_shutdown (ValueProxy[bool]): The flag for other shards to know the bot's gonna
shut down soon.
"""
from payphone.db import setup
from payphone.bot import bot
from payphone.core import config, logging
bot.shard_ids = [shard_id]
bot.shard_count = shard_count
bot.shutdown = shutdown
bot.scheduled_shutdown = scheduled_shutdown
logging.setup(f"Shard {shard_id}")
log.info("Running shard %s with default loop.", shard_id)
bot.loop.set_debug(True)
async def initialize():
await setup.init()
await bot.redis.ping()
bot.loop.run_until_complete(initialize())
bot.run(config.TOKEN)
def flush_process():
import asyncio
from procs.flush_messages import loop
try:
import uvloop # pylint: disable=import-error
uvloop.install()
log.info("Flushing messages with uvloop.")
except ImportError:
log.info("Flushing messages with default loop.")
finally:
asyncio.run(loop())
class ShutdownException(Exception):
"""Raised when a shard has triggered the shutdown of all shards."""
if __name__ == "__main__":
import multiprocessing as mp
import time
from tortoise import run_async
import httpx
import redis.asyncio as aredis
from payphone.db import setup, models
from payphone.core import config
logging.setup("Payphone")
redis = aredis.Redis(host="localhost" if config.IS_DEBUG else "redis")
async def init():
await setup.init()
await models.Call.filter(ended_at=None).update(
ended_at=datetime.now(timezone.utc)
)
await models.Channel.exclude(call_id=None).update(call_id=None)
await redis.flushall()
log.info("Initializing DB and running Redis cleanup...")
run_async(init())
log.info("Cleanup done! Loading recommended shard amount...")
response = httpx.get(
"https://discord.com/api/v10/gateway/bot",
headers={"Authorization": f"Bot {config.TOKEN}"},
)
info = response.json()
shard_count = info["shards"]
manager = mp.Manager()
shutdown = manager.Value("b", False)
scheduled_shutdown = manager.Value("b", False)
log.info("Starting %s shards.", shard_count)
ps: list[mp.Process] = [
mp.Process(
target=shard,
args=(i, shard_count, shutdown, scheduled_shutdown),
)
for i in range(shard_count)
]
flushing_p = mp.Process(target=flush_process)
for i, p in enumerate(ps):
p.start()
log.info("Shard %s's process started.", i)
log.info(
"Waiting 5 seconds start initialize next shard..."
if i != len(ps) - 1
else "All shards were started."
)
if i != len(ps) - 1:
time.sleep(5)
log.info("Starting messages flushing process...")
flushing_p.start()
log.info("Messages flushing process started.")
try:
while True:
if shutdown.value:
raise ShutdownException()
for i in range(shard_count):
if not ps[i].is_alive():
log.warning("Shard %s's process exited. Restarting the shard...", i)
ps[i] = mp.Process(
target=shard,
args=(
i,
shard_count,
shutdown,
scheduled_shutdown,
),
name=f"payphone-shard-{i}",
)
ps[i].start()
log.info(
"Shard %s was restarted. Waiting 5 seconds to check next shard...",
i,
)
time.sleep(5)
if not flushing_p.is_alive():
log.info("Messages flushing process was stopped. Restarting it...")
flushing_p = mp.Process(target=flush_process, name="payphone-flushing")
flushing_p.start()
log.info("Messages flushing process started.")
time.sleep(5)
except KeyboardInterrupt:
log.info("Ctrl-C was pressed.")
except ShutdownException:
log.info("A shard shut down the bot.")
finally:
log.info("Terminating %s shards...", shard_count)
for i, p in enumerate(ps):
p.terminate()
log.info("Terminated shard %s.", i)
log.info("Stopping messages flushing process...")
flushing_p.terminate()
log.info("Checking that all processes have finished...")
for p in ps:
p.join()
flushing_p.join()
log.info("All processes have been terminated.")
+15 -1
View File
@@ -6,7 +6,7 @@ import click
from procs import flush_messages from procs import flush_messages
from payphone.db import setup from payphone.db import setup, models
from payphone.core import logging from payphone.core import logging
@@ -30,7 +30,21 @@ async def main():
@main.command() @main.command()
@coroutine @coroutine
async def flush(): async def flush():
"""Flush all messages in Redis to the database. Runs every 5 seconds."""
while True:
await flush_messages.flush() await flush_messages.flush()
await asyncio.sleep(5)
@main.command()
@coroutine
async def delete_accounts():
"""Delete all accounts that were scheduled for deletion. Runs every 1 hour."""
while True:
await models.User.filter(is_deleting=True).delete()
await asyncio.sleep(60 * 60)
if __name__ == "__main__": if __name__ == "__main__":