Initial MCM project: FastAPI + Textual TUI unified messenger
MultiCustomerMessenger supporting Telegram (python-telegram-bot), WhatsApp (Green API) and SMS (python-gsmmodem-new). REST API with Bearer-token auth, SQLAlchemy models for MariaDB, APScheduler for background polling, and Textual TUI running in same asyncio event-loop.
This commit is contained in:
0
db/__init__.py
Normal file
0
db/__init__.py
Normal file
38
db/database.py
Normal file
38
db/database.py
Normal file
@@ -0,0 +1,38 @@
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.orm import DeclarativeBase, sessionmaker
|
||||
from sqlalchemy.pool import StaticPool
|
||||
from config import settings
|
||||
|
||||
|
||||
class Base(DeclarativeBase):
|
||||
pass
|
||||
|
||||
|
||||
_connect_args = {}
|
||||
_pool_class = None
|
||||
|
||||
if settings.database_url.startswith("sqlite"):
|
||||
_connect_args = {"check_same_thread": False}
|
||||
_pool_class = StaticPool
|
||||
|
||||
engine = create_engine(
|
||||
settings.database_url,
|
||||
connect_args=_connect_args,
|
||||
**({"poolclass": _pool_class} if _pool_class else {}),
|
||||
echo=settings.debug,
|
||||
)
|
||||
|
||||
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
||||
|
||||
|
||||
def get_db():
|
||||
db = SessionLocal()
|
||||
try:
|
||||
yield db
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
def init_db() -> None:
|
||||
from db import models # noqa: F401 – Modelle müssen importiert sein
|
||||
Base.metadata.create_all(bind=engine)
|
||||
144
db/models.py
Normal file
144
db/models.py
Normal file
@@ -0,0 +1,144 @@
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
|
||||
from sqlalchemy import (
|
||||
Boolean,
|
||||
Column,
|
||||
DateTime,
|
||||
Enum,
|
||||
ForeignKey,
|
||||
Index,
|
||||
Integer,
|
||||
String,
|
||||
Table,
|
||||
Text,
|
||||
)
|
||||
from sqlalchemy.orm import relationship
|
||||
from sqlalchemy.sql import func
|
||||
|
||||
from db.database import Base
|
||||
|
||||
|
||||
def _uuid() -> str:
|
||||
return str(uuid.uuid4())
|
||||
|
||||
|
||||
# ── Many-to-many: Conversation ↔ Contact ──────────────────────────────────────
|
||||
conversation_participants = Table(
|
||||
"conversation_participants",
|
||||
Base.metadata,
|
||||
Column("conversation_id", String, ForeignKey("conversations.id", ondelete="CASCADE")),
|
||||
Column("contact_id", String, ForeignKey("contacts.id", ondelete="CASCADE")),
|
||||
)
|
||||
|
||||
|
||||
# ── Contact ────────────────────────────────────────────────────────────────────
|
||||
class Contact(Base):
|
||||
__tablename__ = "contacts"
|
||||
|
||||
id = Column(String, primary_key=True, default=_uuid)
|
||||
name = Column(String(255), nullable=False, index=True)
|
||||
phone = Column(String(32), nullable=True, unique=True)
|
||||
email = Column(String(255), nullable=True)
|
||||
telegram_id = Column(String(64), nullable=True, unique=True)
|
||||
telegram_username = Column(String(255), nullable=True)
|
||||
whatsapp_phone = Column(String(32), nullable=True)
|
||||
notes = Column(Text, nullable=True)
|
||||
created_at = Column(DateTime, default=func.now(), nullable=False)
|
||||
updated_at = Column(DateTime, default=func.now(), onupdate=func.now())
|
||||
|
||||
conversations = relationship(
|
||||
"Conversation",
|
||||
secondary=conversation_participants,
|
||||
back_populates="participants",
|
||||
)
|
||||
messages_sent = relationship(
|
||||
"Message",
|
||||
back_populates="sender",
|
||||
foreign_keys="Message.sender_id",
|
||||
)
|
||||
|
||||
|
||||
# ── Conversation ───────────────────────────────────────────────────────────────
|
||||
class Conversation(Base):
|
||||
__tablename__ = "conversations"
|
||||
|
||||
id = Column(String, primary_key=True, default=_uuid)
|
||||
channel = Column(
|
||||
Enum("telegram", "whatsapp", "sms", name="channel_type"),
|
||||
nullable=False,
|
||||
index=True,
|
||||
)
|
||||
channel_conversation_id = Column(String(255), nullable=True)
|
||||
title = Column(String(255), nullable=True)
|
||||
is_group = Column(Boolean, default=False, nullable=False)
|
||||
is_archived = Column(Boolean, default=False, nullable=False)
|
||||
last_message_at = Column(DateTime, nullable=True)
|
||||
created_at = Column(DateTime, default=func.now(), nullable=False)
|
||||
updated_at = Column(DateTime, default=func.now(), onupdate=func.now())
|
||||
|
||||
participants = relationship(
|
||||
"Contact",
|
||||
secondary=conversation_participants,
|
||||
back_populates="conversations",
|
||||
)
|
||||
messages = relationship(
|
||||
"Message",
|
||||
back_populates="conversation",
|
||||
cascade="all, delete-orphan",
|
||||
order_by="Message.created_at",
|
||||
)
|
||||
|
||||
__table_args__ = (
|
||||
Index("idx_conv_channel_native", "channel", "channel_conversation_id"),
|
||||
)
|
||||
|
||||
|
||||
# ── Message ────────────────────────────────────────────────────────────────────
|
||||
class Message(Base):
|
||||
__tablename__ = "messages"
|
||||
|
||||
id = Column(String, primary_key=True, default=_uuid)
|
||||
conversation_id = Column(
|
||||
String, ForeignKey("conversations.id", ondelete="CASCADE"), nullable=False, index=True
|
||||
)
|
||||
sender_id = Column(String, ForeignKey("contacts.id"), nullable=True)
|
||||
|
||||
channel = Column(
|
||||
Enum("telegram", "whatsapp", "sms", name="channel_type"),
|
||||
nullable=False,
|
||||
index=True,
|
||||
)
|
||||
channel_message_id = Column(String(255), nullable=True)
|
||||
direction = Column(
|
||||
Enum("inbound", "outbound", name="message_direction"),
|
||||
nullable=False,
|
||||
default="outbound",
|
||||
)
|
||||
text = Column(Text, nullable=False)
|
||||
status = Column(
|
||||
Enum("pending", "sent", "delivered", "read", "failed", name="message_status"),
|
||||
default="pending",
|
||||
nullable=False,
|
||||
index=True,
|
||||
)
|
||||
|
||||
reply_to_id = Column(String, ForeignKey("messages.id"), nullable=True)
|
||||
is_edited = Column(Boolean, default=False, nullable=False)
|
||||
error_message = Column(Text, nullable=True)
|
||||
retry_count = Column(Integer, default=0, nullable=False)
|
||||
|
||||
created_at = Column(DateTime, default=func.now(), nullable=False, index=True)
|
||||
updated_at = Column(DateTime, default=func.now(), onupdate=func.now())
|
||||
sent_at = Column(DateTime, nullable=True)
|
||||
delivered_at = Column(DateTime, nullable=True)
|
||||
read_at = Column(DateTime, nullable=True)
|
||||
|
||||
conversation = relationship("Conversation", back_populates="messages")
|
||||
sender = relationship("Contact", back_populates="messages_sent", foreign_keys=[sender_id])
|
||||
reply_to = relationship("Message", remote_side="Message.id", backref="replies")
|
||||
|
||||
__table_args__ = (
|
||||
Index("idx_msg_channel_native", "channel", "channel_message_id"),
|
||||
Index("idx_msg_conv_created", "conversation_id", "created_at"),
|
||||
)
|
||||
Reference in New Issue
Block a user