Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
10 changes: 10 additions & 0 deletions src/tux/database/controllers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
"SnippetController",
"StarboardController",
"StarboardMessageController",
"VerificationController",
]

from tux.database.controllers.afk import AfkController
Expand All @@ -50,6 +51,7 @@
StarboardController,
StarboardMessageController,
)
from tux.database.controllers.verification import VerificationController


class DatabaseCoordinator:
Expand Down Expand Up @@ -113,6 +115,7 @@ def __init__(
self._starboard: StarboardController | None = None
self._starboard_message: StarboardMessageController | None = None
self._reminder: ReminderController | None = None
self._verification: VerificationController | None = None

@property
def guild(self) -> GuildController:
Expand Down Expand Up @@ -209,3 +212,10 @@ def command_permissions(self) -> PermissionCommandController:
cache_backend=getattr(self, "_cache_backend", None),
)
return self._permission_commands

@property
def verification(self) -> VerificationController:
"""Get the verification controller."""
if self._verification is None:
self._verification = VerificationController(self.db)
return self._verification
68 changes: 68 additions & 0 deletions src/tux/database/controllers/verification.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
"""Verification controller for Roblox OAuth2 account linking."""

from __future__ import annotations

from datetime import UTC, datetime
from typing import TYPE_CHECKING

from tux.database.controllers.base import BaseController
from tux.database.models import Verification

if TYPE_CHECKING:
from tux.database.service import DatabaseService


class VerificationController(BaseController[Verification]):
"""Controller for verification-related database operations."""

def __init__(self, db: DatabaseService | None = None) -> None:
"""Initialize the verification controller.

Parameters
----------
db : DatabaseService | None, optional
The database service instance.
"""
super().__init__(Verification, db)

async def get_by_discord_id(self, discord_id: int) -> Verification | None:
"""Get a verification record by Discord ID."""
return await self.find_one(filters=Verification.discord_id == discord_id)

async def get_by_roblox_id(self, roblox_id: int) -> Verification | None:
"""Get a verification record by Roblox ID."""
return await self.find_one(filters=Verification.roblox_id == roblox_id)

async def upsert_verification(
self,
discord_id: int,
roblox_id: int,
roblox_username: str | None = None,
) -> Verification:
"""Create or update a verification record.

Parameters
----------
discord_id : int
Discord user ID.
roblox_id : int
Roblox user ID.
roblox_username : str | None, optional
Roblox username.

Returns
-------
Verification
The created or updated verification record.
"""
result, _ = await self.upsert(
filters={"discord_id": discord_id},
roblox_id=roblox_id,
roblox_username=roblox_username,
verified_at=datetime.now(UTC).replace(tzinfo=None),
)
return result

async def delete(self, discord_id: int) -> bool:
"""Delete a verification record by Discord ID."""
return await self.delete_by_id(discord_id)
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
"""
Revision ID: e938fbc52236
Revises: e7c5ed41fae0
Create Date: 2026-02-26 04:44:00.430858+00:00
"""

from __future__ import annotations

from typing import Sequence, Union

from alembic import op
import sqlalchemy as sa
import sqlmodel


# revision identifiers, used by Alembic.
revision: str = "e938fbc52236"
down_revision: Union[str, None] = "e7c5ed41fae0"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None


def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.create_table(
"verification",
sa.Column(
"created_at",
sa.DateTime(),
server_default=sa.text("CURRENT_TIMESTAMP"),
nullable=True,
),
sa.Column(
"updated_at",
sa.DateTime(),
server_default=sa.text("CURRENT_TIMESTAMP"),
nullable=True,
),
sa.Column("discord_id", sa.BigInteger(), nullable=False),
sa.Column("roblox_id", sa.BigInteger(), nullable=False),
sa.Column(
"roblox_username",
sqlmodel.sql.sqltypes.AutoString(length=100),
nullable=True,
),
sa.Column("verified_at", sa.DateTime(), nullable=False),
sa.CheckConstraint(
"discord_id > 0", name="check_verification_discord_id_valid"
),
sa.CheckConstraint("roblox_id > 0", name="check_verification_roblox_id_valid"),
sa.PrimaryKeyConstraint("discord_id"),
)
with op.batch_alter_table("verification", schema=None) as batch_op:
batch_op.create_index("idx_verification_roblox", ["roblox_id"], unique=False)

with op.batch_alter_table("levels", schema=None) as batch_op:
batch_op.drop_table_comment(existing_comment="f")

# ### end Alembic commands ###


def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table("levels", schema=None) as batch_op:
batch_op.create_table_comment("f", existing_comment=None)

with op.batch_alter_table("verification", schema=None) as batch_op:
batch_op.drop_index("idx_verification_roblox")

op.drop_table("verification")
# ### end Alembic commands ###
2 changes: 2 additions & 0 deletions src/tux/database/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
Snippet,
Starboard,
StarboardMessage,
Verification,
)

__all__ = [
Expand Down Expand Up @@ -51,4 +52,5 @@
# Starboard system
"Starboard",
"StarboardMessage",
"Verification",
]
52 changes: 52 additions & 0 deletions src/tux/database/models/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -1272,3 +1272,55 @@ class StarboardMessage(BaseModel, table=True):
def __repr__(self) -> str:
"""Return string representation showing guild, original message and user."""
return f"<StarboardMessage id={self.id} guild={self.message_guild_id} user={self.message_user_id} channel={self.message_channel_id}>"


# =============================================================================
# VERIFICATION MODELS
# =============================================================================


class Verification(BaseModel, table=True):
"""Linkage between Discord and Roblox accounts.

Stores the verified Roblox account information for each Discord user.

Attributes
----------
discord_id : int
Discord user ID (primary key).
roblox_id : int
Roblox user ID.
roblox_username : str, optional
Roblox username at the time of verification.
verified_at : datetime
Timestamp when the user was verified.
"""

discord_id: int = Field(
primary_key=True,
sa_type=BigInteger,
description="Discord user ID",
)
roblox_id: int = Field(
sa_type=BigInteger,
description="Roblox user ID",
)
roblox_username: str | None = Field(
default=None,
max_length=100,
description="Roblox username",
)
verified_at: datetime = Field(
default_factory=lambda: datetime.now(UTC).replace(tzinfo=None),
description="Timestamp when verification occurred",
)

__table_args__ = (
CheckConstraint("discord_id > 0", name="check_verification_discord_id_valid"),
CheckConstraint("roblox_id > 0", name="check_verification_roblox_id_valid"),
Index("idx_verification_roblox", "roblox_id"),
)

def __repr__(self) -> str:
"""Return string representation showing Discord and Roblox IDs."""
return f"<Verification discord={self.discord_id} roblox={self.roblox_id}>"
Loading
Loading