class ChatParticipant(CRUDMixin, db.Model): __tablename__ = 'chat_participant' chat_id = db.Column( db.ForeignKey( column='chat.id', name='fk_chat_participant_chat_id', onupdate='CASCADE', ondelete='CASCADE' ), primary_key=True ) participant_id = db.Column( db.ForeignKey( column='user.id', name='fk_chat_participant_participant', onupdate='CASCADE', ondelete='CASCADE' ), primary_key=True ) nickname = db.Column(db.String(255), nullable=True, unique=False) __table_args__ = ( db.PrimaryKeyConstraint('chat_id', 'participant_id', name='pk_chat_participant'), ) @property def name(self): return self.nickname or self.participant.profile.name def to_public_json(self): return self.to_json(operations=[('difference', {'chat_id'})])
class GroupChat(CRUDMixin, db.Model): __tablename__ = 'group_chat' id = db.Column( db.ForeignKey( column='chat.id', name='fk_group_chat_chat_id', onupdate='CASCADE', ondelete='CASCADE' ), primary_key=True ) name = db.Column(db.String(255), nullable=True, unique=False)
class MessageText(CRUDMixin, db.Model): __tablename__ = 'message_text' id = db.Column(db.Integer, db.ForeignKey(column='message.id', name='fk_message_text_id', onupdate='CASCADE', ondelete='CASCADE'), primary_key=True) content = db.Column(db.String(512), nullable=False, unique=False) __table_args__ = (db.CheckConstraint('char_length(content) > 0', name='cc_message_text_content'), )
class User(CRUDMixin, db.Model): __tablename__ = 'user' id = db.Column(db.Integer, primary_key=True, autoincrement=True) password = db.Column(db.String(255), nullable=False, unique=False) date_registered = db.Column(db.DateTime, nullable=False, unique=False, server_default=func.now()) date_activated = db.Column(db.DateTime, nullable=True, unique=False) PASSWORD_MINLEN = 8 profile = db.relationship('UserProfile', uselist=False, backref='user', cascade='all, delete', passive_updates=True, passive_deletes=True) conversations = db.relationship('ChatParticipant', uselist=True, backref='participant', cascade='all, delete', passive_updates=True, passive_deletes=True) messages = db.relationship('Message', uselist=True, backref='author', cascade='all, delete', passive_updates=True, passive_deletes=True) @property def is_active(self): return self.date_activated is not None @classmethod def get_first_active(cls, **kwargs): first = cls.get_first(**kwargs) return first if (first and first.is_active) else None def check_password(self, password): return check_password_hash(self.password, password) def to_public_json(self): return self.to_json(operations=[('intersection', {'id', 'date_activated'})])
class Message(CRUDMixin, db.Model): __tablename__ = 'message' id = db.Column(db.Integer, primary_key=True, autoincrement=True) chat_id = db.Column(db.ForeignKey(column='chat.id', name='fk_message_chat_id', onupdate='CASCADE', ondelete='CASCADE'), nullable=False, unique=False) author_id = db.Column(db.ForeignKey(column='user.id', name='fk_message_author_id', onupdate='CASCADE', ondelete='CASCADE'), nullable=False, unique=False) timestamp = db.Column(db.DateTime, nullable=False, unique=False, server_default=func.now()) parent_id = db.Column(db.ForeignKey(column='message.id', name='fk_message_parent_id', onupdate='CASCADE', ondelete='CASCADE'), nullable=True, unique=False) children = db.relationship('Message', uselist=True, backref=db.backref('parent', remote_side=[id]), cascade='all, delete', passive_updates=True, passive_deletes=True) text = db.relationship('MessageText', uselist=False, backref='message', cascade='all, delete', passive_updates=True, passive_deletes=True) def to_json(self, operations=None): return super().to_json(operations) | {'timestamp': str(self.timestamp)} def to_public_json(self): return self.to_json(operations=[( 'difference', {'chat_id'})]) | self.text.to_json() if self.text else {}
class DirectChat(CRUDMixin, db.Model): __tablename__ = 'direct_chat' id = db.Column( db.ForeignKey( column='chat.id', name='fk_direct_chat_chat_id', onupdate='CASCADE', ondelete='CASCADE' ), primary_key=True )
class JWTBlacklist(CRUDMixin, db.Model): __tablename__ = 'jwt_blacklist' id = db.Column(db.Integer, primary_key=True, autoincrement=True) audience = db.Column(db.Integer, db.ForeignKey(column='user.id', name='fk_jwt_blacklist_audience', onupdate='CASCADE', ondelete='SET NULL'), nullable=True, unique=False) jti = db.Column(db.String(255), nullable=False, unique=True) token_type = db.Column(db.String(255), nullable=False, unique=False) issue_date = db.Column(db.DateTime, nullable=False, unique=False) expiration_date = db.Column(db.DateTime, nullable=False, unique=False) @classmethod def insert_if_not_exists(cls, decoded_token): return super().insert_if_not_exists( audience=decoded_token['identity'], jti=decoded_token['jti'], token_type=decoded_token['type'], issue_date=dt.fromtimestamp(decoded_token['iat']), expiration_date=dt.fromtimestamp(decoded_token['exp']))
class UserRelationship(CRUDMixin, db.Model): __tablename__ = 'user_relationship' user_a = db.Column(db.Integer, db.ForeignKey(column='user.id', name='fk_user_relationship_user_a', onupdate='CASCADE', ondelete='CASCADE'), primary_key=True) user_b = db.Column(db.Integer, db.ForeignKey(column='user.id', name='fk_user_relationship_user_b', onupdate='CASCADE', ondelete='CASCADE'), primary_key=True) relation = db.Column(db.String(2), nullable=True, unique=False) since = db.Column(db.DateTime, nullable=True, unique=False) __table_args__ = (db.PrimaryKeyConstraint('user_a', 'user_b', name='pk_user_relation'), db.CheckConstraint('user_a < user_b', name='cc_user_relation_pk'), db.CheckConstraint( f'relation IN ({UserRelation.to_quoted_csv_str()})', name='cc_user_relation_relation')) @validates('user_a', 'user_b') def validates_user_id_pair(self, key, value): lhs = value if key == 'user_a' else self.user_a rhs = value if key == 'user_b' else self.user_b if lhs is not None and rhs is not None and not lhs < rhs: raise ValueError('Value of user_a must be less than user_b') return value @validates('relation') def validates(self, key, value): if value not in UserRelation.to_list(): raise ValueError('Relation must be one of the following values: ' + UserRelation.to_csv_str()) return value @classmethod def sort_user_id_pair(cls, user_id_pair): return { 'user_a': min(user_id_pair[0], user_id_pair[1]), 'user_b': max(user_id_pair[0], user_id_pair[1]) } @classmethod def resolve_friend_requester(cls, sorted_user_id_pair, current_user_id): if sorted_user_id_pair['user_a'] == current_user_id: return UserRelation.FRIEND_REQUEST_FROM_A_TO_B.value else: return UserRelation.FRIEND_REQUEST_FROM_B_TO_A.value @classmethod def resolve_friend_requestee(cls, sorted_user_id_pair, current_user_id): if sorted_user_id_pair['user_a'] == current_user_id: return UserRelation.FRIEND_REQUEST_FROM_B_TO_A.value else: return UserRelation.FRIEND_REQUEST_FROM_A_TO_B.value @classmethod def get_filtered(cls, **kwargs): user_id = kwargs.pop('user_id', None) user_id_pair = kwargs.pop('user_id_pair', None) status = kwargs.pop('status', None) values = deque() if user_id: values.appendleft( cls.query.filter_by(**kwargs).filter( or_(cls.user_a == user_id, cls.user_b == user_id))) return values elif user_id_pair: sorted_user_id_pair = cls.sort_user_id_pair(user_id_pair) kwargs |= sorted_user_id_pair if status: current_user_id = status.get('current_user_id') assert isinstance(current_user_id, int) if status.get('requester'): relation = cls.resolve_friend_requester( sorted_user_id_pair, current_user_id) else: relation = cls.resolve_friend_requestee( sorted_user_id_pair, current_user_id) if status.get('insert'): kwargs['relation'] = relation values.appendleft(relation) values.appendleft(sorted_user_id_pair) values.appendleft(cls.query.filter_by(**kwargs)) return values @classmethod def get_first(cls, **kwargs): filtered, *args = cls.get_filtered(**kwargs) return (filtered.first(), *args) @classmethod def has_row(cls, **kwargs): filtered, *args = cls.get_filtered(**kwargs) return (filtered.scalar() is not None, *args) def other_user(self, user_id): id = self.user_a if self.user_a != user_id else self.user_b return User.get_first(id=id) def user_is_requester(self, user_id): p = 0b10 if self.user_a == user_id else 0b01 q = UserRelation.to_binary(self.relation) return p & q def user_is_requestee(self, user_id): return not self.user_is_requester(user_id) def to_json(self, operations=None): return super().to_json(operations) | {'since': str(self.since)}
class Chat(CRUDMixin, db.Model): __tablename__ = 'chat' id = db.Column(db.Integer, primary_key=True, autoincrement=True) participants = db.relationship( 'ChatParticipant', uselist=True, backref='chat', cascade='all, delete', passive_updates=True, passive_deletes=True ) messages = db.relationship( 'Message', uselist=True, order_by='asc(Message.timestamp)', backref='chat', cascade='all, delete', passive_updates=True, passive_deletes=True, ) direct_chat = db.relationship( 'DirectChat', uselist=False, backref='chat', cascade='all, delete', passive_updates=True, passive_deletes=True ) group_chat = db.relationship( 'GroupChat', uselist=False, backref='chat', cascade='all, delete', passive_updates=True, passive_deletes=True ) @property def latest_message(self): try: return self.messages[-1] except IndexError: return None @classmethod def get_filtered(cls, **kwargs): user_id = kwargs.pop('user_id', None) participant_id_set = kwargs.pop('participant_id_set', None) if user_id is not None: return ( cls.query.filter_by(**kwargs) .filter(cls.participants.any( ChatParticipant.participant_id == user_id )) ) elif participant_id_set: return ( cls.query.filter_by(**kwargs) .join(ChatParticipant) .group_by(cls.id) .having(and_( ( func.count(ChatParticipant.participant_id) == len(participant_id_set) ), func.every( ChatParticipant.participant_id.in_(participant_id_set) ) )) ) else: return cls.query.filter_by(**kwargs) def has_participant(self, participant): for p in self.participants: if p.participant_id == participant.id: return True return False def resolve_name(self, **kwargs): if self.direct_chat or self.group_chat: if self.group_chat and self.group_chat.name: return self.group_chat.name current_user_id = kwargs.get('current_user_id') current_user_name = ( 'CityChat User' if self.direct_chat else 'CityChat Group' ) names = [] for p in self.participants: if p.participant_id != current_user_id: names.append(p.name) else: current_user_name = p.name return ', '.join(names) or current_user_name else: return 'Chat' def to_public_json(self, get_latest_message=False, **kwargs): json = { 'chat': self.to_json() | {'name': self.resolve_name(**kwargs)}, 'participants': { p.participant_id: ( p.to_public_json() | p.participant.profile.to_public_json() ) for p in self.participants } } if get_latest_message: json |= {'latest_message': ( self.latest_message.to_public_json() if self.latest_message else None )} return json