feat: init

This commit is contained in:
h
2026-04-16 01:16:54 +02:00
commit 14bf1047ee
51 changed files with 2227 additions and 0 deletions

4
src/utils/__init__.py Normal file
View File

@@ -0,0 +1,4 @@
from .env import env
from .logging import logger
__all__ = ["env", "logger"]

15
src/utils/db/__init__.py Normal file
View File

@@ -0,0 +1,15 @@
from beanie import init_beanie
from pymongo import AsyncMongoClient
from utils.env import env
client = AsyncMongoClient(env.db.connection_url)
async def init_db():
from .models import Channel, Invite, PendingPost, User
await init_beanie(
database=client[env.db.db_name],
document_models=[User, Channel, Invite, PendingPost],
)

View File

@@ -0,0 +1,6 @@
from .channel import Channel, ChannelMember
from .invite import Invite
from .pending_post import PendingPost
from .user import User
__all__ = ["Channel", "ChannelMember", "Invite", "PendingPost", "User"]

View File

@@ -0,0 +1,58 @@
from typing import Literal
from beanie import Document
from pydantic import BaseModel
class ChannelMember(BaseModel):
user_id: int
display_name: str
mode: Literal["instant", "approval"] = "approval"
class Channel(Document):
id: int
owner_id: int
title: str
members: list[ChannelMember] = []
class Settings:
name = "channels"
@classmethod
async def get_by_id(cls, id_: int) -> "Channel | None":
return await cls.find_one(cls.id == id_)
@classmethod
async def get_user_channels(cls, user_id: int) -> list["Channel"]:
return await cls.find(
{"$or": [{"owner_id": user_id}, {"members.user_id": user_id}]}
).to_list()
def get_member(self, user_id: int) -> ChannelMember | None:
for m in self.members:
if m.user_id == user_id:
return m
return None
async def add_member(
self, user_id: int, display_name: str, mode: Literal["instant", "approval"]
) -> None:
if self.get_member(user_id) is not None:
return
self.members.append(
ChannelMember(user_id=user_id, display_name=display_name, mode=mode)
)
await self.save()
async def remove_member(self, user_id: int) -> None:
self.members = [m for m in self.members if m.user_id != user_id]
await self.save()
async def set_member_mode(
self, user_id: int, mode: Literal["instant", "approval"]
) -> None:
member = self.get_member(user_id)
if member is not None:
member.mode = mode
await self.save()

View File

@@ -0,0 +1,17 @@
from typing import Literal
from beanie import Document
class Invite(Document):
code: str
channel_id: int
mode: Literal["instant", "approval"]
used: bool = False
class Settings:
name = "invites"
@classmethod
async def get_by_code(cls, code: str) -> "Invite | None":
return await cls.find_one(cls.code == code, cls.used == False) # noqa: E712

View File

@@ -0,0 +1,15 @@
from typing import Literal
from beanie import Document
class PendingPost(Document):
source_chat_id: int
source_message_id: int
channel_id: int
contributor_id: int
is_forwarded: bool
status: Literal["pending", "accepted", "rejected"] = "pending"
class Settings:
name = "pending_posts"

View File

@@ -0,0 +1,21 @@
from beanie import Document
class User(Document):
id: int
active_channel_id: int | None = None
class Settings:
name = "users"
@classmethod
async def get_by_id(cls, id_: int) -> "User | None":
return await cls.find_one(cls.id == id_)
@classmethod
async def get_or_create(cls, id_: int) -> "User":
user = await cls.get_by_id(id_)
if user is None:
user = cls(id=id_)
await user.insert()
return user

39
src/utils/env.py Normal file
View File

@@ -0,0 +1,39 @@
from pydantic import SecretStr
from pydantic_settings import BaseSettings, SettingsConfigDict
class BotSettings(BaseSettings):
token: SecretStr
class DatabaseSettings(BaseSettings):
host: str = "mongodb"
port: int = 27017
user: str = "user"
password: str = "password"
db_name: str = "prod"
connection_params: str = "?authSource=admin"
@property
def connection_url(self) -> str:
return f"mongodb://{self.user}:{self.password}@{self.host}:{self.port}/{self.db_name}{self.connection_params}"
class LogSettings(BaseSettings):
level: str = "INFO"
level_external: str = "WARNING"
show_time: bool = False
console_width: int = 150
class Settings(BaseSettings):
bot: BotSettings
db: DatabaseSettings
log: LogSettings
model_config = SettingsConfigDict(
case_sensitive=False, env_file=".env", env_nested_delimiter="__", extra="ignore"
)
env = Settings() # ty:ignore[missing-argument]

36
src/utils/logging.py Normal file
View File

@@ -0,0 +1,36 @@
import logging
from rich.console import Console
from rich.logging import RichHandler
from rich.traceback import install
from .env import env
console = Console(width=env.log.console_width, color_system="auto", force_terminal=True)
def setup_logging() -> None:
from aiogram.dispatcher import router
logging.basicConfig(
level=env.log.level_external,
format="",
datefmt=None,
handlers=[
RichHandler(
console=console,
markup=True,
rich_tracebacks=True,
enable_link_path=False,
tracebacks_show_locals=True,
omit_repeated_times=False,
show_time=env.log.show_time,
tracebacks_suppress=[router],
)
],
)
install(console=console, show_locals=True)
logger = logging.getLogger("post-proposal-bot")
logger.setLevel(env.log.level)