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:
2026-03-03 14:43:19 +01:00
commit 7f3b4768c3
38 changed files with 2072 additions and 0 deletions

144
db/models.py Normal file
View 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"),
)