Skip to content
Draft
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
65 changes: 65 additions & 0 deletions migrations/versions/16ce54fc6920_add_voteban_state.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
"""add_voteban_state

Revision ID: 16ce54fc6920
Revises: 75925089facf
Create Date: 2026-05-03 20:59:51.492122

"""
from typing import Sequence, Union

from alembic import op
import sqlalchemy as sa


# revision identifiers, used by Alembic.
revision: str = '16ce54fc6920'
down_revision: Union[str, Sequence[str], None] = '75925089facf'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None


def upgrade() -> None:
"""Upgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('votebanstate',
sa.Column('guild_id', sa.BigInteger(), nullable=False),
sa.Column('moderator_id', sa.BigInteger(), nullable=False),
sa.Column('user_id', sa.BigInteger(), nullable=False),
sa.Column('reason', sa.String(), nullable=False),
sa.Column('original_duration', sa.String(), nullable=False),
sa.Column('duration', sa.Integer(), nullable=False),
sa.Column('original_delete_messages_per', sa.String(), nullable=True),
sa.Column('delete_messages_per', sa.Integer(), nullable=True),
sa.Column('against_moderators_ids', sa.ARRAY(sa.BigInteger()), nullable=True),
sa.Column('for_moderators_ids', sa.ARRAY(sa.BigInteger()), nullable=True),
sa.Column('attachments_urls', sa.ARRAY(sa.String()), nullable=True),
sa.Column('state', sa.Enum('pending', 'approved', 'denied', name='votebanstateenum', native_enum=False), nullable=False),
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
sa.Column('created_at', sa.DateTime(timezone=True), nullable=False),
sa.Column('updated_at', sa.DateTime(timezone=True), nullable=False),
sa.PrimaryKeyConstraint('id')
)
op.create_index('idx_vote_ban_guild_user_state', 'votebanstate', ['guild_id', 'user_id', 'state'], unique=False)
op.create_index('uq_vote_ban_guild_user_pending', 'votebanstate', ['guild_id', 'user_id'], unique=True, postgresql_where=sa.text("state = 'pending'"))
op.drop_constraint(op.f('uq_guild_channel_role_section'), 'guildforumconfig', type_='unique')
op.create_unique_constraint(None, 'guildforumconfig', ['section_id'])
op.create_unique_constraint(None, 'guildforumconfig', ['role_id'])
op.create_unique_constraint(None, 'guildforumconfig', ['guild_id'])
op.create_unique_constraint(None, 'guildforumconfig', ['channel_id'])
op.create_unique_constraint(None, 'processedforumthread', ['thread_id'])
# ### end Alembic commands ###


def downgrade() -> None:
"""Downgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.drop_constraint(None, 'processedforumthread', type_='unique')
op.drop_constraint(None, 'guildforumconfig', type_='unique')
op.drop_constraint(None, 'guildforumconfig', type_='unique')
op.drop_constraint(None, 'guildforumconfig', type_='unique')
op.drop_constraint(None, 'guildforumconfig', type_='unique')
op.create_unique_constraint(op.f('uq_guild_channel_role_section'), 'guildforumconfig', ['guild_id', 'channel_id', 'role_id', 'section_id'], postgresql_nulls_not_distinct=False)
op.drop_index('uq_vote_ban_guild_user_pending', table_name='votebanstate', postgresql_where=sa.text("state = 'pending'"))
op.drop_index('idx_vote_ban_guild_user_state', table_name='votebanstate')
op.drop_table('votebanstate')
# ### end Alembic commands ###
2 changes: 2 additions & 0 deletions src/infra/db/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
from .ticket import Base, TicketState # noqa: F811
from .transfer_history import Base, TransferHistory # noqa: F811
from .user import Base, User # noqa: F811
from .voteban import Base, VoteBanState # noqa: F811

__all__ = (
"Base",
Expand Down Expand Up @@ -66,4 +67,5 @@
"TicketState",
"TransferHistory",
"User",
"VoteBanState",
)
6 changes: 6 additions & 0 deletions src/infra/db/models/_enums.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,12 @@ class NotifyStateEnum(Enum):
TIMED_OUT = "timed_out"


class VoteBanStateEnum(Enum):
PENDING = "pending"
APPROVED = "approved"
DENIED = "denied"


class ClanMemberRoleEnum(Enum):
LEADER = "leader"
DEPUTY = "deputy"
Expand Down
9 changes: 9 additions & 0 deletions src/infra/db/models/_mixins.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,14 @@ class CreatedAtMixin:
)


class UpdatedAtMixin:
updated_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True),
nullable=False,
default=datetime.now,
onupdate=datetime.now,
)


class IdIntegerMixin:
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
61 changes: 61 additions & 0 deletions src/infra/db/models/voteban.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
"""TicketState model for the Nightcore bot database."""

from sqlalchemy import ARRAY, BigInteger, Enum, Index, String
from sqlalchemy.orm import Mapped, mapped_column
from sqlalchemy.sql import expression

from src.infra.db.models._enums import VoteBanStateEnum
from src.infra.db.models._mixins import (
CreatedAtMixin,
IdIntegerMixin,
UpdatedAtMixin,
)
from src.infra.db.models.base import Base


class VoteBanState(IdIntegerMixin, CreatedAtMixin, UpdatedAtMixin, Base):
guild_id: Mapped[int] = mapped_column(BigInteger, nullable=False)
moderator_id: Mapped[int] = mapped_column(BigInteger, nullable=False)
user_id: Mapped[int] = mapped_column(BigInteger, nullable=False)
reason: Mapped[str] = mapped_column(nullable=False)
original_duration: Mapped[str] = mapped_column(nullable=False)
duration: Mapped[int] = mapped_column(nullable=False)
original_delete_messages_per: Mapped[str] = mapped_column(nullable=True)
delete_messages_per: Mapped[int] = mapped_column(nullable=True)
against_moderators_ids: Mapped[list[int]] = mapped_column(
ARRAY(BigInteger), nullable=True
)
for_moderators_ids: Mapped[list[int]] = mapped_column(
ARRAY(BigInteger), nullable=True
)
attachments_urls: Mapped[list[str]] = mapped_column(
ARRAY(String), nullable=True
)

state: Mapped[VoteBanStateEnum] = mapped_column(
Enum(
VoteBanStateEnum,
native_enum=False,
values_callable=lambda x: [e.value for e in x], # type: ignore
validate_strings=True,
),
nullable=False,
default=VoteBanStateEnum.PENDING,
)

__table_args__ = (
Index(
"uq_vote_ban_guild_user_pending",
"guild_id",
"user_id",
unique=True,
postgresql_where=expression.column("state")
== VoteBanStateEnum.PENDING.value,
),
Index(
"idx_vote_ban_guild_user_state",
"guild_id",
"user_id",
"state",
),
)
16 changes: 16 additions & 0 deletions src/infra/db/operations.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
TicketState,
TransferHistory,
User,
VoteBanState,
)
from src.infra.db.models._annot import (
ModerationStatsResultAnnot,
Expand Down Expand Up @@ -634,6 +635,21 @@ async def get_latest_user_role_request(
return res.scalar_one_or_none()


async def get_voteban_state(
session: AsyncSession, *, guild_id: int, user_id: int
) -> VoteBanState | None:
"""Get latest voteban state for a user and lock row for update."""
stmt = (
select(VoteBanState)
.where(VoteBanState.user_id == user_id)
.order_by(VoteBanState.updated_at.desc().nulls_last())
.limit(1)
.with_for_update()
)
res = await session.execute(stmt)
return res.scalar_one_or_none()


async def get_fraction_roles(
session: AsyncSession, *, guild_id: int
) -> list[str]:
Expand Down
5 changes: 4 additions & 1 deletion src/nightcore/bot.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

import discord
from aiohttp import TCPConnector
from discord import app_commands
from discord import ClientUser, app_commands
from discord.ext.commands import Bot # type: ignore
from nightforo import Client as XenforoClient

Expand Down Expand Up @@ -64,6 +64,8 @@ async def interaction_check(


class Nightcore(Bot):
user: ClientUser # type: ignore

def __init__(
self,
*,
Expand All @@ -74,6 +76,7 @@ def __init__(
self.uow = uow
self.apis = CustomAPICollection()
self.images_cache = ImageCache()
self.config = config

super().__init__(
command_prefix=".",
Expand Down
9 changes: 9 additions & 0 deletions src/nightcore/events/interaction.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@
from src.nightcore.features.meta.components.v2.view.handlers.roleselector import ( # noqa: E501
handle_role_selector_select,
)
from src.nightcore.features.moderation.components.v2.view.handlers import (
handle_voteban_button_callback,
)
from src.nightcore.features.role_requests.components.v2 import (
SendRoleRequestView,
)
Expand Down Expand Up @@ -86,9 +89,15 @@ async def on_interaction(interaction: Interaction["Nightcore"]) -> None: # type
interaction=interaction,
view=SendRoleRequestView,
)

case str() if custom_id.startswith("role_selector:"):
await handle_role_selector_select(interaction=interaction)

case str() if custom_id.startswith("voteban:"):
await handle_voteban_button_callback(
interaction=interaction, custom_id=custom_id
)

case str() if custom_id.startswith("casino:"):
match custom_id:
case "casino:roulette:multiplayer":
Expand Down
Loading