Example #1
0
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)
Example #2
0
        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}
Example #3
0
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
Example #4
0
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))
Example #5
0
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)