class TelegramChatId(ModelMixin, db.Model): """ Model that describes the 'telegram_chat_ids' SQL table Maps telegram chat ids to users """ def __init__(self, *args, **kwargs): """ Initializes the Model :param args: The constructor arguments :param kwargs: The constructor keyword arguments """ super().__init__(*args, **kwargs) __tablename__ = "telegram_chat_ids" """ The name of the table """ user_id: int = db.Column( db.Integer, db.ForeignKey("users.id"), nullable=False ) """ The ID of the user associated with this telegram chat ID """ user: User = db.relationship("User", back_populates="telegram_chat_id") """ The user associated with this telegram chat ID """ chat_id: str = db.Column(db.String(255), nullable=False) """ The telegram chat ID """ def send_message(self, message_text: str): """ Sends a message to the telegram chat :param message_text: The message text to send :return: None """ address = Address(self.chat_id) message = TextMessage( Config.TELEGRAM_BOT_CONNECTION.address, address, message_text ) Config.TELEGRAM_BOT_CONNECTION.send(message)
class Tester(ModelMixin, db.Model): enum = db.Column(db.Enum(A)) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) def __json__(self, include_children: bool = False, _=None) \ -> Dict[str, Any]: return {"id": self.id, "enum": self.enum.value}
class ApiKey(ModelMixin, db.Model): """ Model that describes the 'api_keys' SQL table An ApiKey is used for API access using HTTP basic auth """ def __init__(self, *args, **kwargs): """ Initializes the Model :param args: The constructor arguments :param kwargs: The constructor keyword arguments """ super().__init__(*args, **kwargs) __tablename__ = "api_keys" """ The name of the table """ user_id: int = db.Column(db.Integer, db.ForeignKey("users.id"), nullable=False) """ The ID of the user associated with this API key """ user: User = db.relationship("User", back_populates="api_keys") """ The user associated with this API key """ key_hash: str = db.Column(db.String(255), nullable=False) """ The hash of the API key """ creation_time: int = \ db.Column(db.Integer, nullable=False, default=time.time) """ The time at which this API key was created as a UNIX timestamp """ def has_expired(self) -> bool: """ Checks if the API key has expired. API Keys expire after 30 days :return: True if the key has expired, False otherwise """ return time.time() - self.creation_time > Config.MAX_API_KEY_AGE def verify_key(self, key: str) -> bool: """ Checks if a given key is valid :param key: The key to check :return: True if the key is valid, False otherwise """ try: _id, api_key = key.split(":", 1) if int(_id) != self.id: return False else: return verify_password(api_key, self.key_hash) except ValueError: return False
class ModelMixin: """ A mixin class that specifies a couple of methods all database models should implement """ id = db.Column(db.Integer, primary_key=True, nullable=False, autoincrement=True) """ The ID is the primary key of the table and increments automatically """ def __json__(self, include_children: bool = False, ignore_keys: Optional[List[str]] = None) -> Dict[str, Any]: """ Generates a dictionary containing the information of this model :param include_children: Specifies if children data models will be included or if they're limited to IDs :param ignore_keys: If provided, will not include any of these keys :return: A dictionary representing the model's values """ if ignore_keys is None: ignore_keys = [] json_dict = {} relations: Dict[str, Type] = { key: value.mapper.class_ for key, value in inspect(self.__class__).relationships.items() } for attribute in inspect(self).attrs: key = attribute.key value = attribute.value relation_cls = relations.get(key) if key in ignore_keys: continue elif key.endswith("_hash"): # Skip password hashes etc continue elif isinstance(value, Enum): value = value.name elif relation_cls is not None and \ issubclass(relation_cls, ModelMixin): recursion_keys = [] other_relations = \ list(inspect(relation_cls).relationships.values()) for other_relation in other_relations: other_relation_cls = other_relation.mapper.class_ if other_relation_cls == self.__class__: recursion_keys.append(other_relation.key) recursion_keys += ignore_keys if include_children and value is not None: if isinstance(value, list): value = [ x.__json__(include_children, recursion_keys) for x in value ] else: value = value.__json__(include_children, recursion_keys) elif include_children and value is None: value = None else: continue json_dict[attribute.key] = value return json_dict def __str__(self) -> str: """ :return: The string representation of this object """ data = self.__json__() _id = data.pop("id") return "{}:{} <{}>".format(self.__class__.__name__, _id, str(data)) def __repr__(self) -> str: """ :return: A string with which the object may be generated """ params = "" json_repr = self.__json__() enums = {} for key in json_repr: attr = getattr(self, key) if isinstance(attr, Enum): enum_cls = attr.__class__.__name__ enum_val = attr.name enums[key] = "{}.{}".format(enum_cls, enum_val) for key, val in self.__json__().items(): repr_arg = enums.get(key, repr(val)) params += "{}={}, ".format(key, repr_arg) params = params.rsplit(",", 1)[0] return "{}({})".format(self.__class__.__name__, params) def __eq__(self, other: Any) -> bool: """ Checks the model object for equality with another object :param other: The other object :return: True if the objects are equal, False otherwise """ if "__json__" in dir(other): return other.__json__() == self.__json__() else: return False # pragma: no cover def __hash__(self) -> int: """ Creates a hash so that the model objects can be used as keys :return: None """ return hash(repr(self))
class User(ModelMixin, db.Model): """ Model that describes the 'users' SQL table A User stores a user's information, including their email address, username and password hash """ def __init__(self, *args, **kwargs): """ Initializes the Model :param args: The constructor arguments :param kwargs: The constructor keyword arguments """ super().__init__(*args, **kwargs) __tablename__ = "users" """ The name of the table """ username: str = db.Column(db.String(Config.MAX_USERNAME_LENGTH), nullable=False, unique=True) """ The user's username """ email: str = db.Column(db.String(150), nullable=False, unique=True) """ The user's email address """ password_hash: str = db.Column(db.String(255), nullable=False) """ The user's hashed password, salted and hashed. """ confirmed: bool = db.Column(db.Boolean, nullable=False, default=False) """ The account's confirmation status. Logins should be impossible as long as this value is False. """ confirmation_hash: str = db.Column(db.String(255), nullable=False) """ The account's confirmation hash. This is the hash of a key emailed to the user. Only once the user follows the link in the email containing the key will their account be activated """ telegram_chat_id: Optional["TelegramChatId"] = db.relationship( "TelegramChatId", uselist=False, back_populates="user", cascade="all, delete") """ Telegram chat ID for the user if set up """ api_keys: List["ApiKey"] = db.relationship("ApiKey", back_populates="user", cascade="all, delete") """ API keys for this user """ @property def is_authenticated(self) -> bool: """ Property required by flask-login :return: True if the user is confirmed, False otherwise """ return True @property def is_anonymous(self) -> bool: """ Property required by flask-login :return: True if the user is not confirmed, False otherwise """ return not self.is_authenticated # pragma: no cover @property def is_active(self) -> bool: """ Property required by flask-login :return: True """ return self.confirmed def get_id(self) -> str: """ Method required by flask-login :return: The user's ID as a unicode string """ return str(self.id) def verify_password(self, password: str) -> bool: """ Verifies a password against the password hash :param password: The password to check :return: True if the password matches, False otherwise """ return verify_password(password, self.password_hash) def verify_confirmation(self, confirmation_key: str) -> bool: """ Verifies a confirmation key against the confirmation hash :param confirmation_key: The key to check :return: True if the key matches, False otherwise """ return verify_password(confirmation_key, self.confirmation_hash)