diff --git a/k8s/tasks.yml b/k8s/tasks.yml index 660e824..0b934cc 100644 --- a/k8s/tasks.yml +++ b/k8s/tasks.yml @@ -1,17 +1,38 @@ -apiVersion: batch/v1 -kind: CronJob +apiVersion: apps/v1 +kind: Deployment metadata: name: tasks spec: - schedule: "* * * * *" - jobTemplate: + replicas: 1 + selector: + matchLabels: + app: tasks + template: + metadata: + labels: + app: tasks spec: - template: - spec: - containers: - - name: tasks - image: git.nyeki.dev/payphone/payphone-tasks:latest - command: ["uv", "run", "--no-dev", "tasks.py", "flush"] - restartPolicy: OnFailure - imagePullSecrets: - - name: regcred + containers: + - name: flush + image: git.nyeki.dev/payphone/payphone-tasks:latest + 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 + imagePullSecrets: + - name: regcred diff --git a/payphone/cogs/users/cog.py b/payphone/cogs/users/cog.py index 82dd2dc..b5475cc 100644 --- a/payphone/cogs/users/cog.py +++ b/payphone/cogs/users/cog.py @@ -14,8 +14,9 @@ import httpx from payphone import bot from payphone.db import models, utils -from payphone.cogs import base from payphone.core import constants +from payphone.cogs import base +from payphone.cogs.users import views class Users(base.Cog): @@ -511,7 +512,7 @@ class Users(base.Cog): 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"]) async def leaderboard(self, ctx: commands.Context, board: str = "users") -> None: diff --git a/payphone/cogs/users/views.py b/payphone/cogs/users/views.py new file mode 100644 index 0000000..ad8d384 --- /dev/null +++ b/payphone/cogs/users/views.py @@ -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) diff --git a/payphone/db/migrations/models/17_20241102231436_update.py b/payphone/db/migrations/models/17_20241102231436_update.py new file mode 100644 index 0000000..42550e7 --- /dev/null +++ b/payphone/db/migrations/models/17_20241102231436_update.py @@ -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");;""" diff --git a/payphone/db/models.py b/payphone/db/models.py index 0375ab0..e87acda 100644 --- a/payphone/db/models.py +++ b/payphone/db/models.py @@ -18,6 +18,7 @@ class User(models.Model): username: str = fields.CharField(max_length=32, db_index=True) is_masked: 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) profiles: fields.BackwardFKRelation["Profile"] call_rss: fields.BackwardFKRelation["CallUserChannelRelationship"] diff --git a/run.old.py b/run.old.py deleted file mode 100644 index b4ed4aa..0000000 --- a/run.old.py +++ /dev/null @@ -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.") diff --git a/tasks.py b/tasks.py index 481c5d1..404b4f0 100644 --- a/tasks.py +++ b/tasks.py @@ -6,7 +6,7 @@ import click from procs import flush_messages -from payphone.db import setup +from payphone.db import setup, models from payphone.core import logging @@ -30,7 +30,21 @@ async def main(): @main.command() @coroutine async def flush(): - await flush_messages.flush() + """Flush all messages in Redis to the database. Runs every 5 seconds.""" + + while True: + 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__":