"""Guild-related database models.""" from datetime import datetime from typing import TYPE_CHECKING from sqlalchemy import JSON, Boolean, Float, ForeignKey, Integer, String, Text from sqlalchemy.dialects.postgresql import JSONB from sqlalchemy.orm import Mapped, mapped_column, relationship from guardden.models.base import Base, SnowflakeID, TimestampMixin if TYPE_CHECKING: from guardden.models.moderation import ModerationLog, Strike class Guild(Base, TimestampMixin): """Represents a Discord guild (server) configuration.""" __tablename__ = "guilds" id: Mapped[int] = mapped_column(SnowflakeID, primary_key=True) name: Mapped[str] = mapped_column(String(100), nullable=False) owner_id: Mapped[int] = mapped_column(SnowflakeID, nullable=False) premium: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False) # Relationships settings: Mapped["GuildSettings"] = relationship( back_populates="guild", uselist=False, cascade="all, delete-orphan" ) banned_words: Mapped[list["BannedWord"]] = relationship( back_populates="guild", cascade="all, delete-orphan" ) moderation_logs: Mapped[list["ModerationLog"]] = relationship( back_populates="guild", cascade="all, delete-orphan" ) strikes: Mapped[list["Strike"]] = relationship( back_populates="guild", cascade="all, delete-orphan" ) class GuildSettings(Base, TimestampMixin): """Per-guild bot settings and configuration.""" __tablename__ = "guild_settings" guild_id: Mapped[int] = mapped_column( SnowflakeID, ForeignKey("guilds.id", ondelete="CASCADE"), primary_key=True ) # General settings prefix: Mapped[str] = mapped_column(String(10), default="!", nullable=False) locale: Mapped[str] = mapped_column(String(10), default="en", nullable=False) # Channel configuration (stored as snowflake IDs) log_channel_id: Mapped[int | None] = mapped_column(SnowflakeID, nullable=True) mod_log_channel_id: Mapped[int | None] = mapped_column(SnowflakeID, nullable=True) welcome_channel_id: Mapped[int | None] = mapped_column(SnowflakeID, nullable=True) # Role configuration mute_role_id: Mapped[int | None] = mapped_column(SnowflakeID, nullable=True) verified_role_id: Mapped[int | None] = mapped_column(SnowflakeID, nullable=True) mod_role_ids: Mapped[dict] = mapped_column( JSONB().with_variant(JSON(), "sqlite"), default=list, nullable=False ) # Moderation settings automod_enabled: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False) anti_spam_enabled: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False) link_filter_enabled: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False) # Automod thresholds message_rate_limit: Mapped[int] = mapped_column(Integer, default=5, nullable=False) message_rate_window: Mapped[int] = mapped_column(Integer, default=5, nullable=False) duplicate_threshold: Mapped[int] = mapped_column(Integer, default=3, nullable=False) mention_limit: Mapped[int] = mapped_column(Integer, default=5, nullable=False) mention_rate_limit: Mapped[int] = mapped_column(Integer, default=10, nullable=False) mention_rate_window: Mapped[int] = mapped_column(Integer, default=60, nullable=False) scam_allowlist: Mapped[list[str]] = mapped_column( JSONB().with_variant(JSON(), "sqlite"), default=list, nullable=False ) # Strike thresholds (actions at each threshold) strike_actions: Mapped[dict] = mapped_column( JSONB().with_variant(JSON(), "sqlite"), default=lambda: { "1": {"action": "warn"}, "3": {"action": "timeout", "duration": 3600}, "5": {"action": "kick"}, "7": {"action": "ban"}, }, nullable=False, ) # AI moderation settings ai_moderation_enabled: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False) ai_sensitivity: Mapped[int] = mapped_column(Integer, default=80, nullable=False) # 0-100 scale ai_confidence_threshold: Mapped[float] = mapped_column(Float, default=0.7, nullable=False) ai_log_only: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False) nsfw_detection_enabled: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False) nsfw_only_filtering: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False) # Verification settings verification_enabled: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False) verification_type: Mapped[str] = mapped_column( String(20), default="button", nullable=False ) # button, captcha, questions # Relationship guild: Mapped["Guild"] = relationship(back_populates="settings") class BannedWord(Base, TimestampMixin): """Banned words/phrases for a guild with regex support.""" __tablename__ = "banned_words" id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) guild_id: Mapped[int] = mapped_column( SnowflakeID, ForeignKey("guilds.id", ondelete="CASCADE"), nullable=False ) pattern: Mapped[str] = mapped_column(Text, nullable=False) is_regex: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False) action: Mapped[str] = mapped_column( String(20), default="delete", nullable=False ) # delete, warn, strike reason: Mapped[str | None] = mapped_column(Text, nullable=True) source: Mapped[str | None] = mapped_column(String(100), nullable=True) category: Mapped[str | None] = mapped_column(String(20), nullable=True) managed: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False) # Who added this and when added_by: Mapped[int] = mapped_column(SnowflakeID, nullable=False) # Relationship guild: Mapped["Guild"] = relationship(back_populates="banned_words")