from typing import Any, Iterator from datetime import datetime from contextlib import contextmanager import sqlalchemy as sa from sqlalchemy.orm import declarative_base, sessionmaker, relationship from util.json_type import JSONType import settings def Col(*args: Any, **kwargs: Any) -> sa.Column: if 'nullable' not in kwargs: kwargs['nullable'] = False return sa.Column(*args, **kwargs) class Base(declarative_base()): # type: ignore __abstract__ = True class WithFrontData(Base): __abstract__ = True # Data specific to front-ends; e.g. different types of password hashes # E.g. front_data = { 'msn': { ... }, 'ymsg': { ... }, ... } _front_data = Col(JSONType, name = 'front_data', default = {}) def set_front_data(self, frontend: str, key: str, value: Any) -> None: fd = self._front_data or {} if frontend not in fd: fd[frontend] = {} fd[frontend][key] = value # As a side-effect, this also makes `._front_data` into a new object, # so SQLAlchemy picks up the fact that it's been changed. # (SQLAlchemy only does shallow comparisons on fields by default.) self._front_data = _simplify_json_data(fd) def get_front_data(self, frontend: str, key: str) -> Any: fd = self._front_data if not fd: return None fd = fd.get(frontend) if not fd: return None return fd.get(key) class User(WithFrontData): __tablename__ = 'user' id = Col(sa.Integer, primary_key = True) date_created = Col(sa.DateTime, default = datetime.utcnow) date_login = Col(sa.DateTime, nullable = True) uuid = Col(sa.String(50), unique = True) email = Col(sa.String(80)) username = Col(sa.String(40)) first_name = Col(sa.String(50), default = 'John') middle_name = Col(sa.String(50), nullable = True) last_name = Col(sa.String(50), default = 'Doe') # specifically used for social features/member directory nickname = Col(sa.String(60), nullable = True) uin = Col(sa.BigInteger, nullable = True) # verified for logging in. all existing users pre-email verification have this set to true, until a certain date, unless they verified their email (account_verified), then it's always true. this is set to false on all new users post-verification unless they've also verified their e-mail address verified_to_login = Col(sa.Boolean) # Roaming name - can be null and (in theory) stays constant to what the user sets it to name = Col(sa.String(60), nullable = True) name_last_modified = Col(sa.DateTime, default = datetime.utcnow) # Friendly name set during IM sessions. It cannot be null and is more prone to being overwritten than the roaming name friendly_name = Col(sa.String(60)) # Roaming message message = Col(sa.String(255), nullable = True) message_last_modified = Col(sa.DateTime, default = datetime.utcnow) password = Col(sa.String(250)) groups = Col(JSONType) settings = Col(JSONType) suspended = Col(sa.Boolean) is_tester = Col(sa.Boolean) is_mvp = Col(sa.Boolean) show_in_dir = Col(sa.Boolean) evil_permanent = Col(sa.Integer, default = 0) evil_temporary = Col(sa.Integer, default = 0) alias_active = Col(sa.Boolean, default=False) did_firsttime_email_change = Col(sa.Boolean, default=False) account_verified = Col(sa.Boolean, default=False) # for e-mail address verification lang = Col(sa.String(10), nullable = True) profile = relationship("UserProfile", backref="user", uselist=False, cascade="all, delete-orphan") __table_args__ = (sa.Index('email_ci_index', sa.text('(LOWER(email))'), unique = True), sa.Index('username_ci_index', sa.text('(LOWER(username))'), unique = True)) class UserContact(WithFrontData): __tablename__ = 'user_contact' user_id = Col(sa.Integer, sa.ForeignKey('user.id'), primary_key = True) contact_id = Col(sa.Integer, sa.ForeignKey('user.id'), primary_key = True) user_uuid = Col(sa.String(50), sa.ForeignKey('user.uuid')) # = User(self.user_id).uuid uuid = Col(sa.String(50), sa.ForeignKey('user.uuid')) # = User(self.contact_id).uuid name = Col(sa.String(100)) lists = Col(sa.Integer) pending = Col(sa.Boolean, default = False) groups = Col(JSONType) is_messenger_user = Col(sa.Boolean) index_id = Col(sa.String(50)) birthdate = Col(sa.DateTime, nullable = True) anniversary = Col(sa.DateTime, nullable = True) notes = Col(sa.String(255), nullable = True) first_name = Col(sa.String(50), nullable = True) middle_name = Col(sa.String(50), nullable = True) last_name = Col(sa.String(50), nullable = True) nickname = Col(sa.String(80), nullable = True) primary_email_type = Col(sa.String(10), nullable = True) personal_email = Col(sa.String(80), nullable = True) work_email = Col(sa.String(80), nullable = True) im_email = Col(sa.String(80), nullable = True) other_email = Col(sa.String(80), nullable = True) home_phone = Col(sa.String(50), nullable = True) work_phone = Col(sa.String(50), nullable = True) fax_phone = Col(sa.String(50), nullable = True) pager_phone = Col(sa.String(50), nullable = True) mobile_phone = Col(sa.String(50), nullable = True) other_phone = Col(sa.String(50), nullable = True) personal_website = Col(sa.String(80), nullable = True) business_website = Col(sa.String(80), nullable = True) locations = Col(JSONType, default = {}) class UserProfile(WithFrontData): __tablename__ = 'user_profile' user_id = Col(sa.Integer, sa.ForeignKey('user.id'), primary_key = True) bio = Col(sa.Text, nullable = True) pronouns = Col(sa.String(40), nullable = True) website = Col(sa.String(100), nullable = True) socials = Col(JSONType, nullable = True) streetaddr = Col(sa.String(100), nullable = True) city = Col(sa.String(80), nullable = True) state = Col(sa.String(80), nullable = True) zip = Col(sa.Integer, nullable = True) country = Col(sa.String(60), nullable = True) language = Col(sa.String(50), nullable = True) interests = Col(JSONType, nullable = True, default = {}) visibility = Col(sa.String(25), default='public') class Circle(Base): __tablename__ = 'circle' id = Col(sa.Integer, primary_key = True) chat_id = Col(sa.String(50), unique = True) name = Col(sa.String(100)) owner_id = Col(sa.Integer, sa.ForeignKey('user.id')) owner_uuid = Col(sa.String(50), sa.ForeignKey('user.uuid')) owner_friendly = Col(sa.String(100)) membership_access = Col(sa.Integer) request_membership_option = Col(sa.Integer) class CircleMembership(Base): __tablename__ = 'circle_membership' id = Col(sa.Integer, primary_key = True) chat_id = Col(sa.String(50), sa.ForeignKey('circle.chat_id')) member_id = Col(sa.Integer, sa.ForeignKey('user.id')) member_uuid = Col(sa.String(50), sa.ForeignKey('user.uuid')) role = Col(sa.Integer) state = Col(sa.Integer) blocking = Col(sa.Boolean) inviter_uuid = Col(sa.String(50), nullable = True) inviter_email = Col(sa.String(100), nullable = True) inviter_name = Col(sa.String(100), nullable = True) invite_message = Col(sa.String(250), nullable = True) class Sound(Base): __tablename__ = 'sound' hash = Col(sa.String(50), primary_key = True) title = Col(sa.String(100)) category = Col(sa.Integer) language = Col(sa.Integer) is_public = Col(sa.Boolean) hits = Col(sa.Integer, default = 0) class LoginToken(Base): __tablename__ = 'login_token' id = Col(sa.Integer, primary_key = True) token = Col(sa.String(100)) purpose = Col(sa.String(25)) data = Col(JSONType) expiry = Col(sa.DateTime) def _simplify_json_data(data: Any) -> Any: if isinstance(data, dict): d = {} for k, v in data.items(): v = _simplify_json_data(v) if v is not None: d[k] = v if not d: return None return d if isinstance(data, (list, tuple)): return [_simplify_json_data(x) for x in data] return data engine = sa.create_engine(settings.DB, echo='debug' if settings.DEBUG and settings.DEBUG_FULL and settings.DEBUG_LOG_SQL_QUERIES else None) session_factory = sessionmaker(bind = engine) @contextmanager def Session() -> Iterator[Any]: if Session._depth > 0: # type: ignore yield Session._global # type: ignore return session = session_factory() Session._global = session # type: ignore Session._depth += 1 # type: ignore try: yield session session.commit() except: session.rollback() raise finally: session.close() Session._global = None # type: ignore Session._depth -= 1 # type: ignore Session._global = None # type: ignore Session._depth = 0 # type: ignore