-
Notifications
You must be signed in to change notification settings - Fork 19
Feat: Parental Consent Flow #172
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
73e0394
6c81d2b
fa8d01e
79279e9
489cca3
f6a35a0
c993eee
d142f2e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,48 @@ | ||
| """add minor_report and minor_review_reviewer tables | ||
|
|
||
| Revision ID: 82ea695d0a65 | ||
| Revises: 4fc1c39216c9 | ||
| Create Date: 2026-02-16 12:31:57.651377 | ||
|
|
||
| """ | ||
| from alembic import op | ||
| import sqlalchemy as sa | ||
| from sqlalchemy.dialects import mysql | ||
|
|
||
| # revision identifiers, used by Alembic. | ||
| revision = '82ea695d0a65' | ||
| down_revision = '4fc1c39216c9' | ||
| branch_labels = None | ||
| depends_on = None | ||
|
|
||
|
|
||
| def upgrade() -> None: | ||
| op.create_table( | ||
| "minor_report", | ||
| sa.Column("id", sa.Integer(), nullable=False), | ||
| sa.Column("user_id", mysql.BIGINT(display_width=18), nullable=False), | ||
| sa.Column("reporter_id", mysql.BIGINT(display_width=18), nullable=False), | ||
| sa.Column("suspected_age", sa.Integer(), nullable=False), | ||
| sa.Column("evidence", mysql.TEXT(), nullable=False), | ||
| sa.Column("report_message_id", mysql.BIGINT(display_width=20), nullable=False), | ||
| sa.Column("status", mysql.VARCHAR(length=32), nullable=False, server_default="pending"), | ||
| sa.Column("reviewer_id", mysql.BIGINT(display_width=18), nullable=True), | ||
| sa.Column("created_at", mysql.TIMESTAMP(), nullable=False), | ||
| sa.Column("updated_at", mysql.TIMESTAMP(), nullable=False), | ||
| sa.Column("associated_ban_id", sa.Integer(), nullable=True), | ||
| sa.PrimaryKeyConstraint("id"), | ||
| ) | ||
| op.create_table( | ||
| "minor_review_reviewer", | ||
| sa.Column("id", sa.Integer(), nullable=False), | ||
| sa.Column("user_id", mysql.BIGINT(display_width=18), nullable=False), | ||
| sa.Column("added_by", mysql.BIGINT(display_width=18), nullable=True), | ||
| sa.Column("created_at", mysql.TIMESTAMP(), nullable=False), | ||
| sa.PrimaryKeyConstraint("id"), | ||
| sa.UniqueConstraint("user_id"), | ||
| ) | ||
|
|
||
|
|
||
| def downgrade() -> None: | ||
| op.drop_table("minor_review_reviewer") | ||
| op.drop_table("minor_report") | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,15 +1,22 @@ | ||
| import asyncio | ||
| import logging | ||
| from datetime import datetime, timedelta | ||
| from datetime import datetime, timedelta, timezone | ||
|
|
||
| from discord import Member | ||
| from discord.ext import commands, tasks | ||
| from sqlalchemy import select | ||
|
|
||
| from src import settings | ||
| from src.bot import Bot | ||
| from src.database.models import Ban, Mute | ||
| from src.database.models import Ban, MinorReport, Mute | ||
| from src.database.session import AsyncSessionLocal | ||
| from src.helpers.ban import unban_member, unmute_member | ||
| from src.helpers.minor_verification import ( | ||
| APPROVED, | ||
| CONSENT_VERIFIED, | ||
| assign_minor_role, | ||
| years_until_18, | ||
| ) | ||
| from src.helpers.schedule import schedule | ||
|
|
||
| logger = logging.getLogger(__name__) | ||
|
|
@@ -28,7 +35,7 @@ async def all_tasks(self) -> None: | |
| logger.debug("Gathering scheduled tasks...") | ||
| await self.auto_unban() | ||
| await self.auto_unmute() | ||
| # await asyncio.gather(self.auto_unmute()) | ||
| await self.auto_remove_minor_role() | ||
| logger.debug("Scheduling completed.") | ||
|
|
||
| async def auto_unban(self) -> None: | ||
|
|
@@ -99,6 +106,80 @@ async def auto_unmute(self) -> None: | |
|
|
||
| await asyncio.gather(*unmute_tasks) | ||
|
|
||
| async def auto_remove_minor_role(self) -> None: | ||
| """Remove minor role from users who have reached 18 based on report data.""" | ||
| logger.debug("Checking for minor roles to remove based on age.") | ||
| now = datetime.now(timezone.utc) | ||
|
|
||
| async with AsyncSessionLocal() as session: | ||
| result = await session.scalars( | ||
| select(MinorReport).filter(MinorReport.status.in_([APPROVED, CONSENT_VERIFIED])) | ||
| ) | ||
| reports = result.all() | ||
|
|
||
| for guild_id in settings.guild_ids: | ||
| guild = self.bot.get_guild(guild_id) | ||
| if not guild: | ||
| logger.warning(f"Unable to find guild with ID {guild_id} for minor role cleanup.") | ||
| continue | ||
|
|
||
| for report in reports: | ||
| # Compute approximate 18th birthday based on suspected age at report time. | ||
| created_at = report.created_at | ||
| if created_at.tzinfo is None: | ||
| created_at = created_at.replace(tzinfo=timezone.utc) | ||
| years = years_until_18(report.suspected_age) | ||
| expires_at = created_at + timedelta(days=365 * years) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| if now < expires_at: | ||
| continue | ||
|
|
||
| member: Member | None = await self.bot.get_member_or_user(guild, report.user_id) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| if not member: | ||
| continue | ||
|
|
||
| role_id = settings.roles.VERIFIED_MINOR | ||
| role = guild.get_role(role_id) | ||
| if not role or role not in member.roles: | ||
| continue | ||
|
|
||
| logger.info( | ||
| "Removing minor role from user %s (%s) because they have reached 18.", | ||
| member, | ||
| member.id, | ||
| ) | ||
| try: | ||
| await member.remove_roles(role, atomic=True) | ||
| except Exception as exc: | ||
| logger.warning( | ||
| "Failed to remove minor role from %s (%s): %s", member, member.id, exc | ||
| ) | ||
|
|
||
| @commands.Cog.listener() | ||
| async def on_member_join(self, member: Member) -> None: | ||
| """Assign minor role on rejoin if consent is verified and they are still under 18.""" | ||
| async with AsyncSessionLocal() as session: | ||
| result = await session.scalars( | ||
| select(MinorReport).filter( | ||
| MinorReport.user_id == member.id, | ||
| MinorReport.status == CONSENT_VERIFIED, | ||
| ) | ||
| ) | ||
| report = result.first() | ||
|
|
||
| if not report: | ||
| return | ||
|
|
||
| now = datetime.now(timezone.utc) | ||
| created_at = report.created_at | ||
| if created_at.tzinfo is None: | ||
| created_at = created_at.replace(tzinfo=timezone.utc) | ||
| years = years_until_18(report.suspected_age) | ||
| expires_at = created_at + timedelta(days=365 * years) | ||
| if now >= expires_at: | ||
| return | ||
|
|
||
| await assign_minor_role(member, member.guild) | ||
|
|
||
|
|
||
| def setup(bot: Bot) -> None: | ||
| """Load the `ScheduledTasks` cog.""" | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
associated_ban_idreferencesBan.idbut has noForeignKeyconstraint. Consider adding one withondelete="SET NULL".