feat(bot): ✨ Add "Delete My Activity Data" button to p.me.
This commit is contained in:
+34
-13
@@ -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:
|
||||||
|
matchLabels:
|
||||||
|
app: tasks
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: tasks
|
||||||
spec:
|
spec:
|
||||||
template:
|
containers:
|
||||||
spec:
|
- name: flush
|
||||||
containers:
|
image: git.nyeki.dev/payphone/payphone-tasks:latest
|
||||||
- name: tasks
|
command: ["uv", "run", "--no-dev", "tasks.py", "flush"]
|
||||||
image: git.nyeki.dev/payphone/payphone-tasks:latest
|
resources:
|
||||||
command: ["uv", "run", "--no-dev", "tasks.py", "flush"]
|
requests:
|
||||||
restartPolicy: OnFailure
|
memory: "250Mib"
|
||||||
imagePullSecrets:
|
cpu: "1"
|
||||||
- name: regcred
|
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
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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");;"""
|
||||||
@@ -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
@@ -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.")
|
|
||||||
@@ -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():
|
||||||
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__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
Reference in New Issue
Block a user