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()))
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
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
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()
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}>"
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()
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]
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
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
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
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()
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"))
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)
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()