from flask_sqlalchemy import SQLAlchemy from datetime import datetime, timedelta import os try: import tiktoken TIKTOKEN_AVAILABLE = True except Exception: TIKTOKEN_AVAILABLE = False tiktoken = None import json db = SQLAlchemy() class User(db.Model): """User model for authentication and ownership""" id = db.Column(db.Integer, primary_key=True) email = db.Column(db.String(255), unique=True, nullable=False, index=True) display_name = db.Column(db.String(50), unique=True, nullable=False, index=True) created_at = db.Column(db.DateTime, default=datetime.utcnow, nullable=False) last_login = db.Column(db.DateTime, default=datetime.utcnow, nullable=False) # Relationships owned_rooms = db.relationship('Room', backref='owner', lazy='dynamic', foreign_keys='Room.owner_id') def __repr__(self): return f'' class OTPToken(db.Model): """One-Time Password tokens for email authentication""" id = db.Column(db.Integer, primary_key=True) email = db.Column(db.String(255), nullable=False, index=True) otp_code = db.Column(db.String(6), nullable=False) created_at = db.Column(db.DateTime, default=datetime.utcnow, nullable=False) expires_at = db.Column(db.DateTime, nullable=False) used = db.Column(db.Boolean, default=False, nullable=False) def __init__(self, email, otp_code, expiration_minutes=10): self.email = email self.otp_code = otp_code self.created_at = datetime.utcnow() self.expires_at = self.created_at + timedelta(minutes=expiration_minutes) self.used = False def is_valid(self): """Check if the OTP is still valid (not used and not expired)""" return not self.used and datetime.utcnow() < self.expires_at def __repr__(self): return f'' class Room(db.Model): id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(128), nullable=False, unique=True) title = db.Column(db.String(128), nullable=True) active_users = db.Column(db.Text, default="") # Store as a comma-separated string inactive_users = db.Column(db.Text, default="") # Store as a comma-separated string is_private = db.Column(db.Boolean, default=False, nullable=False, index=True) is_archived = db.Column(db.Boolean, default=False, nullable=False, index=True) owner_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=True, index=True) created_at = db.Column(db.DateTime, default=datetime.utcnow, nullable=False) updated_at = db.Column(db.Integer, default=lambda: int(datetime.utcnow().timestamp()), nullable=False) forked_from_id = db.Column(db.Integer, db.ForeignKey('room.id'), nullable=True) def add_user(self, username): active_users = set(self.active_users.split(",")) if self.active_users else set() inactive_users = ( set(self.inactive_users.split(",")) if self.inactive_users else set() ) # Move from inactive to active if necessary if username in inactive_users: inactive_users.discard(username) active_users.add(username) self.active_users = ",".join(sorted(active_users)) self.inactive_users = ",".join(sorted(inactive_users)) def remove_user(self, username): active_users = set(self.active_users.split(",")) if self.active_users else set() inactive_users = ( set(self.inactive_users.split(",")) if self.inactive_users else set() ) if username in active_users: active_users.discard(username) inactive_users.add(username) # Move to inactive users self.active_users = ",".join(sorted(active_users)) self.inactive_users = ",".join(sorted(inactive_users)) def get_active_users(self): return self.active_users.split(",") if self.active_users else [] def get_inactive_users(self): return self.inactive_users.split(",") if self.inactive_users else [] class UserSession(db.Model): id = db.Column(db.Integer, primary_key=True) session_id = db.Column(db.String(128), unique=True, nullable=False) username = db.Column(db.String(128)) room_name = db.Column(db.String(128)) room_id = db.Column(db.Integer) class Message(db.Model): id = db.Column(db.Integer, primary_key=True) username = db.Column(db.String(128), nullable=False) content = db.Column(db.String(1024), nullable=False) token_count = db.Column(db.Integer) room_id = db.Column(db.Integer, db.ForeignKey("room.id"), nullable=False) def __init__(self, username, content, room_id): self.username = username self.content = content self.room_id = room_id self.count_tokens() def count_tokens(self): if self.token_count is None: if self.is_base64_image(): self.token_count = 0 elif not TIKTOKEN_AVAILABLE: # Fallback: estimate ~4 chars per token when tiktoken unavailable self.token_count = len(self.content) // 4 + 1 else: try: encoding = tiktoken.encoding_for_model("gpt-4") self.token_count = len(encoding.encode(self.content)) except Exception: # Fallback on any tiktoken error (network, SSL, etc.) self.token_count = len(self.content) // 4 + 1 return self.token_count def is_base64_image(self): """Check if message contains a base64-encoded image.""" if not self.content: return False # Check for any base64 image (jpeg, png, gif, webp, etc.) return '