Some checks failed
NSFW-Only Filtering Tests / NSFW-Only Filtering Feature Tests (push) Has been cancelled
- Add nsfw_only_filtering field to GuildSettings model - Create database migration for new field (20260124_add_nsfw_only_filtering) - Update AI moderation logic to respect NSFW-only mode - Add Discord command !ai nsfwonly <true/false> for toggling mode - Implement filtering logic in image analysis for both attachments and embeds - Add comprehensive test suite for new functionality - Update documentation with usage examples and feature description - Create dedicated CI workflow for testing NSFW-only filtering feature When enabled, only sexual/nude content is filtered while allowing: - Violence and gore - Harassment and bullying - Hate speech - Self-harm content - Other content categories This mode is useful for gaming communities and mature discussion servers that have specific content policies allowing violence but prohibiting sexual material.
137 lines
5.8 KiB
Python
137 lines
5.8 KiB
Python
"""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")
|