Example #1
0
class MihkGame(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    num_players = db.Column(db.Integer)
    creator_is_fs = db.Column(db.Boolean)
    allow_extra_roles = db.Column(db.Boolean)
    players = db.relationship("MihkPlayer", backref="game")

    def role_to_str(self, role):
        return {
            0: "Forensic Scientist",
            1: "Murderer",
            2: "Accomplice",
            3: "Witness",
            4: "Investigator",
        }[role]

    def _get_all_roles(self):
        # I'm lazy and hard-coding this... it's bad.
        all_roles = [0, 1]
        if self.num_players > 5 and self.allow_extra_roles:
            all_roles += [2, 3]
        investigators = [4] * (self.num_players - len(all_roles))
        all_roles += investigators
        return all_roles

    def get_role(self, force_fs=False):
        # This is awful...
        if force_fs:
            return 0
        all_roles = self._get_all_roles()
        used_roles = [player.role for player in self.players]
        return random.choice(
            list((collections.Counter(all_roles) -
                  collections.Counter(used_roles)).elements()))
Example #2
0
class SecretResponse(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    secret_id = db.Column(db.Integer, db.ForeignKey("secret.id"))
    person = db.Column(db.String(constants.SECRET_PERSON_NAME_MAX_LEN))
    response = db.Column(db.String(constants.SECRET_RESPONSE_MAX_LEN))

    def __init__(self, secret, person, response):
        self.secret = secret
        self.person = person
        self.response = response
        self.secret.actual_responses += 1
Example #3
0
class Post(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    author_id = db.Column(db.Integer, db.ForeignKey("user.id"))
    comments = db.relationship("Comment",
                               backref="parent_post",
                               lazy="dynamic")
    num_comments = db.Column(db.Integer)
    title = db.Column(db.String(constants.POST_TITLE_MAX_LEN))
    body = db.Column(db.Text)

    posted_at = db.Column(db.DateTime, default=datetime.datetime.utcnow)
    edited_at = db.Column(db.DateTime,
                          index=True,
                          default=datetime.datetime.utcnow)
    last_activity = db.Column(db.DateTime,
                              index=True,
                              default=datetime.datetime.utcnow)

    show_anon = db.Column(db.Boolean)

    followers = db.relationship("User",
                                secondary="post_follow",
                                lazy="dynamic")

    def __init__(self, author, title, body, show_anon):
        self.author_id = author.id
        self.title = title
        self.body = body
        self.num_comments = 0
        self.show_anon = show_anon

    def edit(self, new_body, new_anon_policy):
        if self.body == new_body and self.show_anon == new_anon_policy:
            return
        self.body = new_body
        self.show_anon = new_anon_policy
        self.edited_at = datetime.datetime.now()
        self.last_activity = datetime.datetime.now()

    def notify_followers(self, poster):
        url = flask.url_for("forum.view_post", post_id=self.id)
        for follower in self.followers:
            # Don't notify the comment author
            if follower.id != poster.id:
                follower.notify(constants.NEW_COMMENT_NOTIF_STR, url)

    @property
    def html_body(self):
        result = utils.safe_html(self.body)
        return markdown.markdown(result, extensions=["extra", "codehilite"])

    @property
    def was_edited(self):
        diff = abs(self.posted_at - self.edited_at)
        return diff.seconds > 0
Example #4
0
class Secret(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    shortname = db.Column(db.String(constants.SECRET_SHORTNAME_MAX_LEN),
                          index=True)
    responses = db.relationship("SecretResponse", backref="secret")
    expected_responses = db.Column(db.Integer)
    actual_responses = db.Column(db.Integer)
    created_at = db.Column(db.DateTime, default=datetime.datetime.utcnow)

    def __init__(self, shortname, expected_responses):
        self.shortname = shortname
        self.expected_responses = expected_responses
        self.actual_responses = 0

    @classmethod
    def get_by_shortname(cls, shortname):
        return cls.query.filter_by(shortname=shortname).first()
Example #5
0
class LearnQuestion(db.Model, search.SearchableMixin):
    __searchable__ = ["page_name", "question", "answer"]
    id = db.Column(db.Integer, primary_key=True)
    page_name = db.Column(db.String(constants.LEARNPAGE_MAX_LEN), index=True)
    question = db.Column(db.Text)
    answer = db.Column(db.Text)

    asker_id = db.Column(db.Integer, db.ForeignKey("user.id"))
    asker = db.relationship("User", foreign_keys=[asker_id])
    show_anon = db.Column(db.Boolean)

    good_question = db.Column(db.Boolean, index=True, default=False)

    def __init__(self, page_name, question, asker, show_anon):
        self.page_name = page_name
        self.question = question
        self.asker = asker
        self.show_anon = show_anon

    def submit_answer(self, answer_text, mark_as_good=False):
        self.answer = answer_text
        self.good_question = mark_as_good

    @property
    def html_question(self):
        return markdown.markdown(self.question,
                                 extensions=["extra", "codehilite"])

    @property
    def html_answer(self):
        return markdown.markdown(self.answer,
                                 extensions=["extra", "codehilite"])

    def __repr__(self):
        return f"<Question {self.id}>"
Example #6
0
class Warning(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    user_id = db.Column(db.Integer, db.ForeignKey("user.id"))
    reason = db.Column(db.String(constants.WARNING_MAX_LEN))
    timestamp = db.Column(db.DateTime)

    def __init__(self, user, reason):
        self.user = user
        self.reason = reason
        self.timestamp = datetime.datetime.utcnow()
        self.send_notification()

    def send_notification(self):
        notification = profile_models.Notification(
            recipient=self.user,
            message=self.reason,
            icon=constants.WARNING_ICON,
            text_class=constants.WARNING_TEXT_CLASS,
        )
        db.session.add(notification)
        db.session.commit()
Example #7
0
class BugReport(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    user_id = db.Column(db.Integer, db.ForeignKey("user.id"))
    report_type = db.Column(db.Integer)
    text_response = db.Column(db.Text)
    submitted_at = db.Column(db.DateTime, default=datetime.datetime.utcnow)

    user = db.relationship("User", foreign_keys=[user_id])

    def __init__(self, user, report_type, text_response):
        if report_type not in REPORT_TYPES.keys():
            raise ValueError(
                f"Invalid report type ({report_type}) not in {REPORT_TYPES.keys()}"
            )
        self.user = user
        self.report_type = report_type
        self.text_response = text_response

    @property
    def report_type_str(self):
        return REPORT_TYPES[self.report_type]
Example #8
0
class LearnPageStats(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    page_name = db.Column(db.String(constants.LEARNPAGE_MAX_LEN), index=True)
    views = db.Column(db.Integer)

    def __init__(self, page_name):
        self.page_name = page_name
        self.views = 0

    @classmethod
    def get_by_name(cls, name):
        return cls.query.filter_by(page_name=name).first()

    @classmethod
    def get_or_create(cls, name):
        filepath = os.path.join(constants.LEARN_PAGE_TEMPLATE_DIR, name)
        template_fullpath = os.path.join(constants.TEMPLATE_DIR, filepath)
        page_stats = cls.get_by_name(name)

        if page_stats is None and os.path.exists(template_fullpath):
            page_stats = cls(name)
            db.session.add(page_stats)
            db.session.commit()
        return page_stats
Example #9
0
class Comment(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    post_id = db.Column(db.Integer, db.ForeignKey("post.id"))
    author_id = db.Column(db.Integer, db.ForeignKey("user.id"))
    comment_idx = db.Column(db.Integer)
    body = db.Column(db.Text)

    posted_at = db.Column(db.DateTime,
                          index=True,
                          default=datetime.datetime.utcnow)
    edited_at = db.Column(db.DateTime, default=datetime.datetime.utcnow)

    show_anon = db.Column(db.Boolean)

    def __init__(self, post, author, body, show_anon):
        self.post_id = post.id
        # comment_idx helps with redirects after an edit
        post.num_comments += 1
        self.comment_idx = post.num_comments
        self.author_id = author.id
        self.body = body
        self.show_anon = show_anon

    def edit(self, new_body, new_anon_policy):
        if self.body == new_body and self.show_anon == new_anon_policy:
            return
        self.body = new_body
        self.show_anon = new_anon_policy
        self.edited_at = datetime.datetime.utcnow()

    @property
    def html_body(self):
        result = utils.safe_html(self.body)
        return markdown.markdown(result, extensions=["extra", "codehilite"])

    @property
    def was_edited(self):
        diff = abs(self.posted_at - self.edited_at)
        return diff.seconds > 0
Example #10
0
class Task(db.Model):
    id = db.Column(db.String(constants.TASK_ID_LEN), primary_key=True)
    name = db.Column(db.String(constants.TASK_NAME_MAX_LEN), index=True)
    description = db.Column(db.String(constants.TASK_DESC_MAX_LEN))
    meta = db.Column(db.Text)
    user_id = db.Column(db.Integer, db.ForeignKey("user.id"))
    complete = db.Column(db.Boolean, default=False)

    def get_rq_job(self):
        try:
            return rq.job.Job.fetch(self.id,
                                    connection=flask.current_app.redis)
        except (redis.exceptions.RedisError, rq.exceptions.NoSuchJobError):
            return None

    def get_progress(self):
        job = self.get_rq_job()
        return job.meta.get('progress', 0) if job is not None else 100
Example #11
0
class Notification(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    recipient_id = db.Column(db.Integer, db.ForeignKey("user.id"))
    message = db.Column(db.String(constants.NOTIFICATION_MAX_LEN))
    url_link = db.Column(db.String(constants.NOTIFICATION_URL_LINK_MAX_LEN))
    icon = db.Column(db.String(constants.ICON_MAX_LEN))
    text_class = db.Column(db.String(constants.TEXT_CLASS_MAX_LEN))
    timestamp = db.Column(db.DateTime, index=True, default=datetime.datetime.utcnow)

    def __init__(self, recipient, message, url_link=None, text_class=None, icon=None):
        self.recipient = recipient
        self.message = message
        self.url_link = url_link
        self.text_class = text_class
        self.icon = icon
        self.send()

    def send(self):
        if self.recipient.unread_notifications is None:
            self.recipient.unread_notifications = 0
        self.recipient.unread_notifications += 1
        db.session.commit()
Example #12
0
class PostFollow(db.Model):
    __tablename__ = "post_follow"
    id = db.Column(db.Integer, primary_key=True)
    user_id = db.Column(db.Integer, db.ForeignKey("user.id"))
    post_id = db.Column(db.Integer, db.ForeignKey("post.id"))
Example #13
0
class MihkPlayer(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(constants.MIHK_PLAYER_NAME_MAX_LEN))
    game_id = db.Column(db.Integer, db.ForeignKey("mihk_game.id"))
    role = db.Column(db.Integer)
Example #14
0
class User(db.Model, flask_login.UserMixin):
    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String(constants.USERNAME_MAX_LEN))
    email = db.Column(db.String(constants.EMAIL_MAX_LEN),
                      index=True,
                      unique=True)
    pw_hash = db.Column(db.String(constants.PW_HASH_LEN))
    email_verified = db.Column(db.Boolean, default=False)
    is_admin = db.Column(db.Boolean, default=False)
    is_banned = db.Column(db.Boolean, default=False)

    warnings = db.relationship("Warning", backref="user", lazy="dynamic")

    notifications = db.relationship("Notification",
                                    backref="recipient",
                                    lazy="dynamic")
    posts = db.relationship("Post", backref="author", lazy="dynamic")
    comments = db.relationship("Comment", backref="author", lazy="dynamic")
    last_notification_read_time = db.Column(db.DateTime)
    unread_notifications = db.Column(db.Integer, default=0)

    tasks = db.relationship("Task", backref="user", lazy="dynamic")
    followed_posts = db.relationship("Post",
                                     secondary="post_follow",
                                     lazy="dynamic")

    def __init__(self, username, email, password):
        flask_login.UserMixin.__init__(self)
        self.username = username
        self.email = email

        pw_bytes = bytes(password, encoding="utf-8")
        sha256 = hashlib.sha256(pw_bytes).hexdigest()
        self.pw_hash = bcrypt.generate_password_hash(sha256).decode("utf-8")
        self.last_notification_read_time = datetime.datetime.utcnow()
        self.send_verify_account_email()

    def set_password(self, new_password):
        pw_bytes = bytes(new_password, encoding="utf-8")
        sha256 = hashlib.sha256(pw_bytes).hexdigest()
        self.pw_hash = bcrypt.generate_password_hash(sha256).decode("utf-8")

    def check_password(self, password):
        pw_bytes = bytes(password, encoding="utf-8")
        sha256 = hashlib.sha256(pw_bytes).hexdigest()
        return bcrypt.check_password_hash(self.pw_hash, sha256)

    def gen_token(self, kind, exp_seconds=None):
        blob = {kind: self.id}
        if exp_seconds is not None:
            blob["exp"] = time.time() + exp_seconds
        return jwt.encode(
            blob,
            flask.current_app.config["SECRET_KEY"],
            algorithm="HS256",
        ).decode("utf-8")

    def notify(self, message, url_link=None, text_class=None, icon=None):
        notif = profile_models.Notification(
            recipient=self,
            message=message,
            url_link=url_link,
            text_class=text_class,
            icon=icon,
        )
        db.session.add(notif)
        db.session.commit()

    def set_notification_read_time(self):
        self.unread_notifications = 0
        self.last_notification_read_time = datetime.datetime.utcnow()
        db.session.commit()

    def new_notifications(self):
        return self.notifications.filter(
            profile_models.Notification.timestamp >
            self.last_notification_read_time).count()

    def set_banned(self, is_banned):
        if is_banned:
            self.notify(
                message="You have been banned for your recent behavior",
                text_class="text-danger",
                icon="fas fa-ban",
            )
        else:
            self.notify(
                message="You have been unbanned",
                text_class="text-success",
                icon="fas fa-badge-check",
            )

        self.is_banned = is_banned
        db.session.add(notif)
        db.session.commit()

    def _launch_task(self, task_name, description, *args, **kwargs):
        if task_name not in flask.current_app.registered_tasks:
            raise NameError(f"Task {task_name} has not been registered")

        rq_job = flask.current_app.task_queue.enqueue(
            constants.TASK_PREFIX + task_name, *args, **kwargs)
        task = base_models.Task(id=rq_job.get_id(),
                                name=task_name,
                                description=description,
                                user=self)
        db.session.add(task)
        db.session.commit()
        return task

    def get_tasks_in_progress(self):
        return base_models.Task.query.filter_by(user=self,
                                                complete=False).all()

    def get_task_in_progress(self, name):
        return base_models.Task.query.filter_by(name=name,
                                                user=self,
                                                complete=False).first()

    @classmethod
    def gen_user_for_token(cls, kind, token):
        try:
            obj = jwt.decode(
                token,
                flask.current_app.config["SECRET_KEY"],
                algorithms=["HS256"],
            )
            flask.current_app.logger.info(f"jwt decode: {obj}")
            user_id = obj[kind]
        except Exception as e:
            flask.current_app.logger.warning(f"jwt decode exception: {e}")
            return None
        return cls.query.get(user_id)

    def send_verify_account_email(self):
        token = self.gen_token(constants.VERIFY_ACCOUNT_TOKEN_STR)

        self._launch_task(
            task_name="send_email",
            description=f"Email verification for {self.email}",
            # func args below
            email_props={
                "subject":
                constants.VERIFY_ACCOUNT_SUBJECT_STR,
                "sender":
                flask.current_app.config["ADMIN"],
                "recipients": [self.email],
                "text_body":
                flask.render_template(
                    "auth/email/verify_account.txt",
                    user=self,
                    token=token,
                ),
                "html_body":
                flask.render_template(
                    "auth/email/verify_account.html",
                    user=self,
                    token=token,
                ),
            },
        )

    def verify_email(self):
        self.email_verified = True
        db.session.commit()

    def send_reset_password_email(self):
        # 24 hours
        token = self.gen_token(
            constants.RESET_PASSWORD_TOKEN_STR,
            exp_seconds=constants.PW_RESET_EXP_SECONDS,
        )

        self._launch_task(
            task_name="send_email",
            description=f"Password reset email for user_id={self.id}",
            # func args below
            email_props={
                "subject":
                constants.RESET_PASSWORD_SUBJECT_STR,
                "sender":
                flask.current_app.config["ADMIN"],
                "recipients": [self.email],
                "text_body":
                flask.render_template(
                    "auth/email/reset_password.txt",
                    user=self,
                    token=token,
                ),
                "html_body":
                flask.render_template(
                    "auth/email/reset_password.html",
                    user=self,
                    token=token,
                ),
            },
        )

    def get_new_notifications(self):
        last_read_time = self.last_notification_read_time or datetime.datetime(
            1900, 1, 1)
        self.last_notification_read_time = datetime.datetime.utcnow()
        return profile_models.Notification.query.filter_by(
            recipient=self).filter(
                profile_models.Notification.timestamp > last_read_time).limit(
                    10)

    def record_view(self, page_name):
        self._launch_task(
            task_name="record_view",
            description=f"Record learn page view for {page_name}",
            # func args below
            username=self.username,
            page_name=page_name,
        )

    def __repr__(self):
        return f"<User {self.id}: {self.username}>"

    @classmethod
    def get_by_email(cls, email):
        return cls.query.filter_by(email=email).first()