Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .test.env
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ SENTRY_DSN=

# Channels
CHANNEL_SR_MOD=1127695218900993410
CHANNEL_MINOR_REVIEW=1437472925720280084
CHANNEL_VERIFY_LOGS=1012769518828339331
CHANNEL_BOT_COMMANDS=1276953350848588101
CHANNEL_SPOILER=2769521890099371011
Expand All @@ -35,6 +36,7 @@ CHANNEL_HOW_TO_VERIFY=1432333413980835840

# Roles
ROLE_VERIFIED=1333333333333333337
ROLE_VERIFIED_MINOR=1281517925395733615

ROLE_BIZCTF2022=7629466241011276950
ROLE_NOAH_GANG=6706800691011276950
Expand Down Expand Up @@ -101,3 +103,7 @@ SLACK_FEEDBACK_WEBHOOK="https://hook.slack.com/sdfsdfsf"

#Feedback Webhook
JIRA_WEBHOOK="https://automation.atlassian.com/sdfsdfsf"

# Nexus API
NEXUS_API_BASE_URL="https://hackthebox.com/"
NEXUS_API_TOKEN="test_nexus_token"
2 changes: 1 addition & 1 deletion CONTRIBUTORS
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,4 @@ TheWeeknd <98106526+ToxicBiohazard@users.noreply.github.com>
Dimosthenis Schizas <dimos@hackthebox.eu>
makelarisjr <8687447+makelarisjr@users.noreply.github.com>
Jelle Janssens <janssensjelle@users.noreply.github.com>
Ryan Gordon <ry4n@hackthebox.eu>
Ryan Gordon <ry4n@hackthebox.eu>
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@
Create Date: 2023-05-09 13:28:06.763604

"""
from alembic import op
from sqlalchemy.dialects import mysql

from alembic import op

# revision identifiers, used by Alembic.
revision = '05c1d218bb76'
down_revision = '6948a2436536'
Expand Down
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),
Comment thread
ToxicBiohazard marked this conversation as resolved.
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
Expand Up @@ -5,9 +5,10 @@
Create Date: 2023-05-09 13:34:17.055796

"""
from alembic import op
from sqlalchemy.dialects import mysql

from alembic import op

# revision identifiers, used by Alembic.
revision = 'a5f283a4cfde'
down_revision = '05c1d218bb76'
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
"""add FK constraint on minor_report.associated_ban_id

Revision ID: c3f1a2b4d5e6
Revises: 82ea695d0a65
Create Date: 2026-04-25 00:00:00.000000

"""
from alembic import op

# revision identifiers, used by Alembic.
revision = 'c3f1a2b4d5e6'
down_revision = '82ea695d0a65'
branch_labels = None
depends_on = None


def upgrade() -> None:
op.create_foreign_key(
"fk_minor_report_associated_ban_id",
"minor_report",
"ban",
["associated_ban_id"],
["id"],
ondelete="SET NULL",
)


def downgrade() -> None:
op.drop_constraint(
"fk_minor_report_associated_ban_id",
"minor_report",
type_="foreignkey",
)
2 changes: 1 addition & 1 deletion contributors.sh
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,4 @@ TheWeeknd <98106526+ToxicBiohazard@users.noreply.github.com>

EOF

git log --format='%aN <%aE>' | sort -uf >> "$file"
git log --format='%aN <%aE>' | sort -uf >> "$file"
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ dependencies = [
"audioop-lts>=0.2.2,<0.3.0",
"aiohttp>=3.13.4",
"urllib3>=2.6.3",
"python-dateutil>=2.8.0,<3.0.0",
]

[dependency-groups]
Expand Down
88 changes: 85 additions & 3 deletions src/cmds/automation/scheduled_tasks.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,23 @@
import asyncio
import logging
from datetime import datetime, timedelta
from datetime import datetime, timedelta, timezone

from dateutil.relativedelta import relativedelta
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__)
Expand All @@ -28,7 +36,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:
Expand Down Expand Up @@ -99,6 +107,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 exact 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 + relativedelta(years=years)
if now < expires_at:
continue

member = await self.bot.get_member_or_user(guild, report.user_id)
if not isinstance(member, 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 + relativedelta(years=years)
if now >= expires_at:
return

await assign_minor_role(member, member.guild)


def setup(bot: Bot) -> None:
"""Load the `ScheduledTasks` cog."""
Expand Down
Loading