class Statistic24hLog(BaseModel): id = BlobField(primary_key=True) time = MyTimestampField(index=True) data = BinaryJSONField(dumps=json_ex_dumps) class Meta: db_table = 'statistic24h_log'
class PostModel(BaseModel): id = BlobField( primary_key=True, constraints=[SQL("DEFAULT int2bytea(nextval('id_gen_seq'))")]) state = IntegerField(default=POST_STATE.NORMAL, index=True) visible = IntegerField(default=POST_VISIBLE.NORMAL, index=True) time = MyTimestampField(index=True, default=get_time, help_text='发布时间') user_id = BlobField(index=True, null=True, default=None) # 发布用户,对 user 表来说是推荐者,对 board 来说是创建者 # is_for_tests = BooleanField(default=False, help_text='单元测试标记,单元测试结束后删除') @classmethod @abstractmethod def get_post_type(cls): """ 获取类型ID :return: """ pass @abstractmethod def get_title(self): pass @classmethod def append_post_id(cls, values): """ 若有ID生成器,那么向values中添加生成出的值,若生成器为SQL Serial,则什么都不做 :param values: :return: """ if config.POST_ID_GENERATOR != config.SQLSerialGenerator: values['id'] = config.POST_ID_GENERATOR().to_bin()
class Notification(BaseModel): id = BlobField(primary_key=True) sender_ids = ArrayField(BlobField) receiver_id = BlobField(index=True) type = IntegerField(index=True) time = MyTimestampField(index=True) # 发布时间 data = BinaryJSONField(dumps=json_ex_dumps) is_read = BooleanField(default=False) @classmethod def count(cls, user_id): return cls.select().where(cls.receiver_id == user_id, cls.is_read == False).count() @classmethod def set_read(cls, user_id): cur = db.execute_sql( ''' WITH updated_rows as ( UPDATE notif SET is_read = TRUE WHERE "receiver_id" = %s AND "is_read" = FALSE RETURNING is_read ) SELECT count(is_read) FROM updated_rows; ''', (user_id, )) return cur.fetchone()[0] @classmethod def refresh(cls, user_id, cooldown=config.NOTIF_FETCH_COOLDOWN): new = [] r: UserNotifRecord = UserNotifRecord.get_by_pk(user_id) if not r: return if cooldown and (time.time() - r.update_time < cooldown): return for i in r.get_notifications(True): if i['type'] == NOTIF_TYPE.BE_COMMENTED: new.append({ 'id': config.LONG_ID_GENERATOR().to_bin(), 'sender_ids': (i['comment']['user']['id'], ), 'receiver_id': user_id, 'type': NOTIF_TYPE.BE_COMMENTED, 'time': i['time'], 'data': i, }) elif i['type'] == NOTIF_TYPE.BE_REPLIED: new.append({ 'id': config.LONG_ID_GENERATOR().to_bin(), 'sender_ids': (i['comment']['user']['id'], ), 'receiver_id': user_id, 'type': NOTIF_TYPE.BE_REPLIED, 'time': i['time'], 'data': i, }) if new: cls.insert_many(new).execute() return len(new) class Meta: db_table = 'notif'
class Follow(BaseModel): id = BlobField(primary_key=True) related_id = BlobField(index=True) # 被关注对象 related_type = IntegerField(index=True) # 被关注对象的类型 user_id = BlobField(index=True) # 用户 time = MyTimestampField(index=True) # 发布时间 class Meta: db_table = 'follow'
class PostModel(BaseModel): id = BlobField(primary_key=True, constraints=[SQL("DEFAULT int2bytea(nextval('id_gen_seq'))")]) state = IntegerField(default=POST_STATE.NORMAL, index=True) visible = IntegerField(default=POST_VISIBLE.NORMAL, index=True) time = MyTimestampField(index=True) # 发布时间 user_id = BlobField(index=True, null=True, default=None) # 发布用户,对 user 表来说是推荐者,对 board 来说是创建者 @abstractmethod def get_title(self): pass
class Topic(PostModel): title = TextField(index=True) board_id = BlobField(index=True) edit_count = IntegerField(default=0) edit_time = MyTimestampField(index=True, null=True) last_edit_user_id = BlobField(index=True, null=True) content = TextField() awesome = IntegerField(default=0) # 精华文章 sticky_weight = IntegerField(index=True, default=0) # 置顶权重 weight = IntegerField(index=True, default=0) # 排序权值,越大越靠前,默认权重与id相同 update_time = BigIntegerField(index=True, null=True, default=None) # 更新时间,即发布的时间或最后被回复的时间 # comment_time = BigIntegerField(index=True, null=True, default=None) # 更新时间,即发布的时间或最后被回复的时间 # object_type = OBJECT_TYPES.TOPIC class Meta: db_table = 'topic' @classmethod async def weight_redis_init(cls): cur = db.execute_sql('select max(weight)+1 from "topic"') await redis.set(RK_TOPIC_WEIGHT_MAX, cur.fetchone()[0] or 0) @classmethod async def weight_gen(cls): """ 提升一点权重上限""" return int(await redis.incr(RK_TOPIC_WEIGHT_MAX)) async def weight_inc(self): """ 提升一点排序权重,但不能高于最大权重 """ self.weight = min(self.weight + 1, int(await redis.get(RK_TOPIC_WEIGHT_MAX))) self.update_time = int(time.time()) self.save() ''' async def comment_update(self): self.comment_time = int(time.time()) self.save() ''' @classmethod def get_post_type(cls): return POST_TYPES.TOPIC def get_title(self): return self.title
class WikiArticle(BaseModel): id = BlobField(primary_key=True) user_id = BlobField(index=True) time = MyTimestampField(index=True) state = IntegerField(default=POST_STATE.APPLY, index=True) visible = IntegerField(default=POST_VISIBLE.NORMAL, index=True) major_ver = IntegerField() minor_ver = IntegerField() parent = IntegerField(index=True, null=True) # wiki article root = IntegerField(index=True) # wiki item content = TextField() class Meta: db_table = 'wiki_article'
class Topic(PostModel): title = TextField(index=True) board_id = BlobField(index=True) edit_count = IntegerField(default=0) edit_time = MyTimestampField(index=True, null=True) last_edit_user_id = BlobField(index=True, null=True) content = TextField() awesome = IntegerField(default=0) # 精华文章 sticky_weight = IntegerField(index=True, default=0) # 置顶权重 weight = IntegerField(index=True, default=0) # 排序权值,越大越靠前,默认权重与id相同 # object_type = OBJECT_TYPES.TOPIC class Meta: db_table = 'topic' @classmethod def weight_gen(cls): cur = db.execute_sql('select max(weight)+1 from "topic"') return cur.fetchone()[0] or 0 def weight_inc(self): """ 提升一点排序权重 """ try: db.execute_sql( """ WITH t1 as (SELECT "id", "weight" FROM "topic" WHERE "id" = %s), t2 as (SELECT "t2"."id", "t2"."weight" FROM t1, "topic" AS t2 WHERE "t2"."weight" > "t1".weight ORDER BY "weight" ASC LIMIT 1) UPDATE "topic" set "weight" = ( CASE WHEN "topic"."id" = "t1"."id" THEN "t2"."weight" ELSE "t1"."weight" END ) FROM t1, t2 WHERE "topic"."id" in ("t1"."id", "t2"."id"); """, (self.id, )) except DatabaseError: pass
class UserNotifLastInfo(BaseModel): id = BlobField(primary_key=True) # user_id last_be_commented_id = BlobField(default=b'\x00') last_be_replied_id = BlobField(default=b'\x00') last_be_followed_id = BlobField(default=b'\x00') last_be_mentioned_id = BlobField(default=b'\x00') last_be_bookmarked_id = BlobField(default=b'\x00') last_be_liked_id = BlobField(default=b'\x00') last_be_sent_pm_id = BlobField(default=b'\x00') last_received_sysmsg_id = BlobField(default=b'\x00') last_manage_log_id = BlobField(default=b'\x00') update_time = MyTimestampField(index=True) @classmethod def new(cls, user_id): try: return cls.create(id=user_id, update_time=int(time.time())) except IntegrityError: db.rollback() def get_notifications(self, update_last=False): lst = [] l1 = tuple(fetch_notif_of_comment(self.id, self.last_be_commented_id)) l2 = tuple(fetch_notif_of_reply(self.id, self.last_be_replied_id)) l3 = tuple(fetch_notif_of_metion(self.id, self.last_be_mentioned_id)) l4 = tuple(fetch_notif_of_log(self.id, self.last_manage_log_id)) lst.extend(l1) lst.extend(l2) lst.extend(l3) lst.extend(l4) if update_last: if l1: self.last_be_commented_id = l1[0]['from_post_id'] if l2: self.last_be_replied_id = l2[0]['from_post_id'] if l3: self.last_be_mentioned_id = l3[0]['from_post_id'] if l4: self.last_manage_log_id = l4[0]['from_post_id'] self.update_time = int(time.time()) self.save() return lst class Meta: db_table = 'user_notif_last_info'
class UserNotifRecord(BaseModel): id = BlobField(primary_key=True) # user_id last_comment_id = BlobField(default=b'\x00') last_reply_id = BlobField(default=b'\x00') last_follow_id = BlobField(default=b'\x00') last_mention_id = BlobField(default=b'\x00') last_bookmark_id = BlobField(default=b'\x00') last_like_id = BlobField(default=b'\x00') last_pm_id = BlobField(default=b'\x00') last_sysmsg_id = BlobField(default=b'\x00') update_time = MyTimestampField(index=True) @classmethod def new(cls, user_id): try: return cls.create(id=user_id, update_time=int(time.time())) except IntegrityError: db.rollback() def get_notifications(self, update_last=False): lst = [] l1 = tuple(fetch_notif_of_comment(self.id, self.last_comment_id)) l2 = tuple(fetch_notif_of_reply(self.id, self.last_reply_id)) l3 = tuple(fetch_notif_of_metion(self.id, self.last_mention_id)) lst.extend(l1) lst.extend(l2) lst.extend(l3) # lst.sort(key = lambda x: x['time'], reverse=True) if update_last: if l1: self.last_comment_id = l1[0]['comment']['id'] if l2: self.last_reply_id = l2[0]['comment']['id'] if l3: self.last_mention_id = l3[0]['mention']['id'] self.update_time = int(time.time()) self.save() return lst class Meta: db_table = 'user_notif_record'
class User(PostModel, BaseUser): email = TextField(index=True, unique=True) nickname = CITextField(index=True, unique=True) # CITextField password = BlobField() salt = BlobField() # auto biology = TextField(null=True) # 简介 avatar = TextField(null=True) type = IntegerField(default=0) # 账户类型,0默认,1组织 url = TextField(null=True) # 个人主页 location = TextField(null=True) # 所在地 # level = IntegerField(index=True) # 用户级别 group = IntegerField(index=True, default=USER_GROUP.NORMAL) # 用户权限组 key = BlobField(index=True, null=True) key_time = MyTimestampField() # 最后登录时间 access_time = MyTimestampField(null=True, default=None) # 最后访问时间,以misc为准吧 last_check_in_time = MyTimestampField(null=True, default=None) # 上次签到时间 check_in_his = IntegerField(default=0) # 连续签到天数 phone = TextField(null=True, default=None) # 大陆地区 number = SerialField( ) # 序号,第N个用户,注意这有个问题是不能写sequence='user_count_seq',应该是peewee的bug credit = IntegerField(default=0) # 积分,会消费 exp = IntegerField(default=0) # 经验值,不会消失 reputation = IntegerField(default=0) # 声望 ip_registered = INETField(default=None, null=True) # 注册IP reset_key = BlobField(index=True, null=True, default=None) # 重置密码所用key class Meta: db_table = 'user' #object_type = OBJECT_TYPES.USER @classmethod def find_by_nicknames(cls, names): it = iter(names) try: condition = cls.nickname == next(it) except StopIteration: return [] for i in it: condition |= cls.nickname == i return cls.select().where(condition) @property def roles(self): """ 这里角色权限由低到高 :return: """ ret = [None] if self.state == POST_STATE.DEL: return ret if self.group >= USER_GROUP.BAN: ret.append('banned_user') if self.group >= USER_GROUP.INACTIVE: ret.append('inactive_user') if self.group >= USER_GROUP.NORMAL: ret.append('user') if self.group >= USER_GROUP.SUPERUSER: ret.append('superuser') if self.group >= USER_GROUP.ADMIN: ret.append('admin') return ret @classmethod def gen_id(cls): return config.POST_ID_GENERATOR() @classmethod def gen_password_and_salt(cls, password_text): if config.USER_SECURE_AUTH_ENABLE: salt = os.urandom(32) dk = hashlib.pbkdf2_hmac( config.PASSWORD_SECURE_HASH_FUNC_NAME, password_text.encode('utf-8'), salt, config.PASSWORD_SECURE_HASH_ITERATIONS, ) return {'password': dk, 'salt': salt} else: salt = os.urandom(16) m = hmac.new(salt, digestmod=config.PASSWORD_HASH_FUNC) m.update(password_text.encode('utf-8')) return {'password': m.digest(), 'salt': salt} @classmethod def gen_key(cls): key = os.urandom(16) key_time = int(time.time()) return {'key': key, 'key_time': key_time} def refresh_key(self): count = 0 while count < 10: with db.atomic(): try: k = self.gen_key() self.key = k['key'] self.key_time = k['key_time'] self.save() return k except DatabaseError: count += 1 db.rollback() raise ValueError("generate key failed") @classmethod def get_by_key(cls, key): try: return cls.get(cls.key == key) except DoesNotExist: return None async def can_request_actcode(self): """ 是否能申请帐户激活码(用于发送激活邮件) :return: """ if self.group != USER_GROUP.INACTIVE: return val = await redis.get(RK_USER_LAST_REQUEST_ACTCODE_BY_USER_ID % self.id ) if val is None: return True if time.time() - int(val) > config.USER_ACTIVATION_REQUEST_INTERVAL: return True async def gen_activation_code(self) -> bytes: """ 生成一个账户激活码 :return: """ t = int(time.time()) code = os.urandom(8) pipe = redis.pipeline() pipe.set(RK_USER_LAST_REQUEST_ACTCODE_BY_USER_ID % self.id, t) pipe.set(RK_USER_ACTCODE_BY_USER_ID % self.id, code, expire=config.USER_ACTCODE_EXPIRE) await pipe.execute() return code @classmethod async def check_actcode(cls, uid, code): """ 检查账户激活码是否可用,若可用,激活账户 :param uid: :param code: :return: """ if not code: return if isinstance(uid, str): uid = to_bin(uid) if isinstance(code, str): code = to_bin(code) if len(code) == 8: rkey = RK_USER_ACTCODE_BY_USER_ID % uid if await redis.get(rkey) == code: try: u = cls.get(cls.id == uid, cls.group == USER_GROUP.INACTIVE) u.group = USER_GROUP.NORMAL u.save() await redis.delete(rkey) return u except cls.DoesNotExist: pass async def can_request_reset_password(self): """ 是否能申请重置密码 :return: """ val = await redis.get(RK_USER_LAST_REQUEST_RESET_KEY_BY_USER_ID % self.id) if val is None: return True if time.time() - int(val) > config.USER_RESET_PASSWORD_CODE_EXPIRE: return True def gen_reset_key(self) -> bytes: """ 生成一个重置密码key :return: """ # len == 16 + 8 == 24 t = int(time.time()) code = os.urandom(16) + t.to_bytes(8, 'little') redis.set(RK_USER_LAST_REQUEST_RESET_KEY_BY_USER_ID % self.id, t, expire=config.USER_RESET_PASSWORD_REQUST_INTERVAL) redis.set(RK_USER_RESET_KEY_BY_USER_ID % self.id, code, expire=config.USER_RESET_PASSWORD_CODE_EXPIRE) return code @classmethod async def check_reset_key(cls, uid, code) -> Union['User', None]: """ 检查uid与code这一组密码重置密钥是否有效 :param uid: :param code: :return: """ if not code: return if isinstance(uid, str): uid = to_bin(uid) if isinstance(code, str): code = to_bin(code) if len(code) == 24: rkey = RK_USER_RESET_KEY_BY_USER_ID % uid if await redis.get(rkey) == code: try: u = cls.get(cls.id == uid) await redis.delete(rkey) return u except cls.DoesNotExist: pass def set_password(self, new_password): info = self.gen_password_and_salt(new_password) self.salt = info['salt'] self.password = info['password'] self.save() def update_access_time(self): self.access_time = int(time.time()) self.save() return self.access_time def check_in(self): self.last_check_in_time = self.last_check_in_time or 0 old_time = self.last_check_in_time last_midnight = get_today_start_timestamp() # 今日未签到 if self.last_check_in_time < last_midnight: self.last_check_in_time = int(time.time()) # 三天内有签到,连击 if old_time > last_midnight - config.USER_CHECKIN_COUNTER_INTERVAL: self.check_in_his += 1 else: self.check_in_his = 1 self.save() # 签到加分 credit = self.credit exp = self.exp self.credit += 5 self.exp += 5 self.save() ManageLog.add_by_credit_changed_sys(self, note='每日签到', value=[credit, self.credit]) ManageLog.add_by_exp_changed_sys(self, note='每日签到', value=[exp, self.exp]) return { 'credit': 5, 'exp': 5, 'time': self.last_check_in_time, 'check_in_his': self.check_in_his } def daily_access_reward(self): self.access_time = self.access_time or 0 old_time = self.access_time self.update_access_time() if old_time < get_today_start_timestamp(): exp = self.exp self.exp += 5 self.save() ManageLog.add_by_exp_changed_sys(self, note='每日登录', value=[exp, self.exp]) return 5 @classmethod def auth(cls, email, password_text): try: u = cls.get(cls.email == email) except DoesNotExist: return False if config.USER_SECURE_AUTH_ENABLE: dk = hashlib.pbkdf2_hmac( config.PASSWORD_SECURE_HASH_FUNC_NAME, password_text.encode('utf-8'), u.salt.tobytes(), config.PASSWORD_SECURE_HASH_ITERATIONS, ) if u.password.tobytes() == dk: return u else: m = hmac.new(u.salt.tobytes(), digestmod=config.PASSWORD_HASH_FUNC) m.update(password_text.encode('utf-8')) if u.password.tobytes() == m.digest(): return u def __repr__(self): return '<User id:%x nickname:%r>' % (int.from_bytes( self.id.tobytes(), 'big'), self.nickname) def get_title(self): return self.nickname
class User(PostModel, BaseUser): email = TextField(index=True, unique=True, null=True, default=None) phone = TextField(index=True, unique=True, null=True, default=None) # 大陆地区 nickname = CITextField(index=True, unique=True, null=True, help_text='用户昵称') # CITextField password = BlobField() salt = BlobField() # auto biology = TextField(null=True) # 简介 avatar = TextField(null=True) type = IntegerField(default=0) # 账户类型,0默认,1组织 url = TextField(null=True) # 个人主页 location = TextField(null=True) # 所在地 # level = IntegerField(index=True) # 用户级别 group = IntegerField(index=True, default=USER_GROUP.NORMAL) # 用户权限组 is_wiki_editor = BooleanField(default=False, index=True) # 是否为wiki编辑 is_board_moderator = BooleanField(default=False, index=True) # 是否为版主 is_forum_master = BooleanField(default=False, index=True) # 超版 access_time = MyTimestampField(null=True, default=None) # 最后访问时间,以misc为准吧 last_check_in_time = MyTimestampField(null=True, default=None) # 上次签到时间 check_in_his = IntegerField(default=0) # 连续签到天数 number = SerialField( ) # 序号,第N个用户,注意这有个问题是不能写sequence='user_count_seq',应该是peewee的bug credit = IntegerField(default=0) # 积分,会消费 exp = IntegerField(default=0) # 经验值,不会消失 repute = IntegerField(default=0) # 声望 ip_registered = INETField(default=None, null=True) # 注册IP # ref_github = TextField(null=True) # ref_zhihu = TextField(null=True) # ref_weibo = TextField(null=True) is_new_user = BooleanField(default=True) # 是否全新用户(刚注册,未经过修改昵称) phone_verified = BooleanField(default=False) # 手机号已确认 change_nickname_chance = IntegerField(default=0, help_text='改名机会数量') # 改名机会数量 reset_key = BlobField(index=True, null=True, default=None, help_text='重置密码所用key') # 重置密码所用key class Meta: db_table = 'user' #object_type = OBJECT_TYPES.USER @classmethod def new(cls, nickname, password, extra_values=None, *, auto_nickname=False, is_for_tests=True) -> Optional['User']: values = { 'nickname': nickname, 'is_new_user': True # 'is_for_tests': is_for_tests } values.update(extra_values) cls.append_post_id(values) info = cls.gen_password_and_salt(password) values.update(info) try: uid = cls.insert(values).execute() u: User = cls.get_by_pk(uid) uchanged = False # 如果是第一个用户,那么自动为管理员 if u.number == 1: u.group = USER_GROUP.ADMIN uchanged = True # 注册成功后,如果要求自动设置用户名,那么修改用户名 if auto_nickname: nprefix = config.USER_NICKNAME_AUTO_PREFIX + '_' u.change_nickname_chance = 1 u.nickname = nprefix + uid.to_hex() uchanged = True if uchanged: u.save() return u except peewee.IntegrityError: traceback.print_exc() db.rollback() # if e.args[0].startswith('duplicate key | 错误: 重复键违反唯一约束'): # return # 此处似乎无从得知,数据库会返回什么样的文本,应该是和语言相关 # 那么姑且假定 IntegrityError 都是唯一性约束 except peewee.DatabaseError: db.rollback() @classmethod def find_by_nicknames(cls, names): it = iter(names) try: condition = cls.nickname == next(it) except StopIteration: return [] for i in it: condition |= cls.nickname == i return cls.select().where(condition) @property def main_role(self): """ 主要用户角色 """ roles = set(self.roles) for i in MAIN_ROLE_ORDER: if i in roles: return i @property def roles(self): """ 这里角色权限由低到高 :return: """ ret = [None] if self.state == POST_STATE.DEL: return ret if self.group >= USER_GROUP.BAN: ret.append('banned_user') if self.group >= USER_GROUP.INACTIVE: ret.append('inactive_user') if self.group >= USER_GROUP.NORMAL: ret.append('user') if self.is_wiki_editor: ret.append('wiki_editor') if self.is_board_moderator: ret.append('board_moderator') if self.is_forum_master: ret.append('forum_master') if self.group >= USER_GROUP.SUPERUSER: ret.append('superuser') if self.group >= USER_GROUP.ADMIN: ret.append('admin') return ret @classmethod def gen_password_and_salt(cls, password_text): salt = os.urandom(32) dk = hashlib.pbkdf2_hmac( config.PASSWORD_SECURE_HASH_FUNC_NAME, password_text.encode('utf-8'), salt, config.PASSWORD_SECURE_HASH_ITERATIONS, ) return {'password': dk, 'salt': salt} @classmethod def get_by_key(cls, key): try: return cls.get(cls.key == key) except DoesNotExist: return None @classmethod async def gen_reg_code_by_email(cls, email: str, password: str): t = int(time.time()) code = os.urandom(8) email = email.encode('utf-8') pipe = redis.pipeline() pipe.set(RK_USER_REG_CODE_AVAILABLE_TIMES_BY_EMAIL % email, config.USER_REG_CODE_AVAILABLE_TIMES_BY_EMAIL, expire=config.USER_REG_CODE_EXPIRE) pipe.set(RK_USER_REG_CODE_BY_EMAIL % email, code, expire=config.USER_REG_CODE_EXPIRE) pipe.set(RK_USER_REG_PASSWORD % email, password, expire=config.USER_REG_CODE_EXPIRE) await pipe.execute() return code @classmethod async def reg_code_cleanup(cls, email): email = email.encode('utf-8') pipe = redis.pipeline() pipe.delete(RK_USER_REG_CODE_BY_EMAIL % email) pipe.delete(RK_USER_REG_CODE_AVAILABLE_TIMES_BY_EMAIL % email) pipe.delete(RK_USER_REG_PASSWORD % email) await pipe.execute() @classmethod async def check_reg_code_by_email(cls, email, code: Union[str, bytes]): """ 检查账户激活码是否可用 :param uid: :param code: :return: """ if not code: return if isinstance(code, str): code = to_bin(code) if len(code) == 8: email_bytes = email.encode('utf-8') rk_code = RK_USER_REG_CODE_BY_EMAIL % email_bytes rk_times = RK_USER_REG_CODE_AVAILABLE_TIMES_BY_EMAIL % email_bytes rk_pw = RK_USER_REG_PASSWORD % email_bytes if await redis.get(rk_code) == code: # 检查可用次数,decr的返回值是执行后的 if int(await redis.decr(rk_times)) <= 0: return await cls.reg_code_cleanup(email) # 无问题,取出储存值 return (await redis.get(rk_pw)).decode('utf-8') async def can_request_reset_password(self): """ 是否能申请重置密码 :return: """ val = await redis.get(RK_USER_LAST_REQUEST_RESET_KEY_BY_USER_ID % self.id) if val is None: return True if time.time() - int(val) > config.USER_RESET_PASSWORD_CODE_EXPIRE: return True def gen_reset_key(self) -> bytes: """ 生成一个重置密码key :return: """ # len == 16 + 8 == 24 t = int(time.time()) code = os.urandom(16) + t.to_bytes(8, 'little') redis.set(RK_USER_LAST_REQUEST_RESET_KEY_BY_USER_ID % self.id, t, expire=config.USER_RESET_PASSWORD_REQUST_INTERVAL) redis.set(RK_USER_RESET_KEY_BY_USER_ID % self.id, code, expire=config.USER_RESET_PASSWORD_CODE_EXPIRE) return code @classmethod async def check_reset_key(cls, uid, code) -> Union['User', None]: """ 检查uid与code这一组密码重置密钥是否有效 :param uid: :param code: :return: """ if not code: return if isinstance(uid, str): uid = to_bin(uid) if isinstance(code, str): code = to_bin(code) if len(code) == 24: rkey = RK_USER_RESET_KEY_BY_USER_ID % uid if await redis.get(rkey) == code: try: u = cls.get(cls.id == uid) await redis.delete(rkey) return u except cls.DoesNotExist: pass def set_password(self, new_password): info = self.gen_password_and_salt(new_password) self.salt = info['salt'] self.password = info['password'] self.save() def update_access_time(self): self.access_time = int(time.time()) self.save() return self.access_time def check_in(self): self.last_check_in_time = self.last_check_in_time or 0 old_time = self.last_check_in_time last_midnight = get_today_start_timestamp() # 今日未签到 if self.last_check_in_time < last_midnight: self.last_check_in_time = int(time.time()) # 三天内有签到,连击 if old_time > last_midnight - config.USER_CHECKIN_COUNTER_INTERVAL: self.check_in_his += 1 else: self.check_in_his = 1 self.save() # 签到加分 credit = self.credit exp = self.exp self.credit += 5 self.exp += 5 self.save() ManageLog.add_by_credit_changed_sys(get_bytes_from_blob(self.id), credit, self.credit, note='每日签到') ManageLog.add_by_exp_changed_sys(get_bytes_from_blob(self.id), exp, self.exp, note='每日签到') return { 'credit': 5, 'exp': 5, 'time': self.last_check_in_time, 'check_in_his': self.check_in_his } def daily_access_reward(self): self.access_time = self.access_time or 0 old_time = self.access_time self.update_access_time() if old_time < get_today_start_timestamp(): exp = self.exp self.exp += 5 self.save() ManageLog.add_by_exp_changed_sys(get_bytes_from_blob(self.id), exp, self.exp, note='每日登录') return {'exp': 5} def _auth_base(self, password_text): """ 已获取了用户对象,进行密码校验 :param password_text: :return: """ dk = hashlib.pbkdf2_hmac( config.PASSWORD_SECURE_HASH_FUNC_NAME, password_text.encode('utf-8'), get_bytes_from_blob(self.salt), config.PASSWORD_SECURE_HASH_ITERATIONS, ) if get_bytes_from_blob(self.password) == dk: return self @classmethod def auth_by_mail(cls, email, password_text) -> ["User", bool]: try: u = cls.get(cls.email == email) except DoesNotExist: return None, False return u, u._auth_base(password_text) @classmethod def auth_by_username(cls, username, password_text) -> ["User", bool]: try: u = cls.get(cls.username == username) except DoesNotExist: return None, False return u, u._auth_base(password_text) def __repr__(self): return '<User id:%x nickname:%r>' % (int.from_bytes( get_bytes_from_blob(self.id), 'big'), self.nickname) @classmethod def get_post_type(cls): return POST_TYPES.USER def get_title(self): return self.nickname
class ManageLog(BaseModel): id = BlobField(primary_key=True) # 使用长ID user_id = BlobField(index=True, null=True) # 操作用户 role = TextField(null=True) # 操作身份 time = MyTimestampField(index=True) # 操作时间 related_type = IntegerField() # 被操作对象类型 related_id = BlobField(index=True) # 被操作对象 related_user_id = BlobField(index=True, null=True) # 附加关联对象涉及用户 operation = IntegerField() # 操作行为 value = BinaryJSONField(dumps=json_ex_dumps, null=True) # 操作数据 note = TextField(null=True, default=None) @classmethod def new(cls, user_id, role, related_type, related_id, related_user_id, operation, value, note=None): return cls.create(id=config.LONG_ID_GENERATOR().digest(), user_id=user_id, role=role, time=int(time.time()), related_type=related_type, related_id=related_id, related_user_id=related_user_id, operation=operation, value=value, note=note) @classmethod def add_by_credit_changed(cls, view, changed_user, note=None, *, value=None): if view: user_id = view.current_user.id role = view.current_role else: user_id = None role = None return cls.new(user_id, role, POST_TYPES.USER, changed_user.id, changed_user.id, MANAGE_OPERATION.USER_CREDIT_CHANGE, value, note=note) @classmethod def add_by_credit_changed_sys(cls, changed_user, note=None, *, value=None): return cls.add_by_credit_changed(None, changed_user, note, value=value) @classmethod def add_by_reputation_changed(cls, view, changed_user, note=None, *, value=None): if view: user_id = view.current_user.id role = view.current_role else: user_id = None role = None return cls.new(user_id, role, POST_TYPES.USER, changed_user.id, changed_user.id, MANAGE_OPERATION.USER_REPUTATION_CHANGE, value, note=note) @classmethod def add_by_reputation_changed_sys(cls, changed_user, note=None, *, value=None): return cls.add_by_reputation_changed(None, changed_user, note, value=value) @classmethod def add_by_post_changed(cls, view, key, operation, related_type, values, old_record, record, note=None, *, value=NotImplemented): if key in values: if value is NotImplemented: value = [old_record[key], record[key]] return cls.new(view.current_user.id, view.current_role, related_type, record['id'], record['user_id'], operation, value, note=note) class Meta: db_table = 'manage_log'
class ManageLog(BaseModel): id = BlobField(primary_key=True) # 使用长ID user_id = BlobField(index=True, null=True) # 操作用户 role = TextField(null=True) # 操作身份 time = MyTimestampField(index=True) # 操作时间 related_type = IntegerField() # 被操作对象类型 related_id = BlobField(index=True) # 被操作对象 related_user_id = BlobField(index=True, null=True) # 附加关联对象涉及用户 operation = IntegerField() # 操作行为 value = BinaryJSONField(dumps=json_ex_dumps, null=True) # 操作数据 note = TextField(null=True, default=None) @classmethod def new(cls, user_id, role, related_type, related_id, related_user_id, operation, value, note=None): return cls.create(id=config.LONG_ID_GENERATOR().digest(), user_id=user_id, role=role, time=int(time.time()), related_type=related_type, related_id=related_id, related_user_id=related_user_id, operation=operation, value=value, note=note) @classmethod def add_by_credit_changed(cls, view, changed_user, note=None, *, value=None): if view: user_id = view.current_user.id role = view.current_role else: user_id = None role = None return cls.new(user_id, role, POST_TYPES.USER, changed_user.id, changed_user.id, MANAGE_OPERATION.USER_CREDIT_CHANGE, value, note=note) @classmethod def add_by_credit_changed_sys(cls, changed_user, note=None, *, value=None): return cls.add_by_credit_changed(None, changed_user, note, value=value) @classmethod def add_by_repute_changed(cls, view, changed_user, note=None, *, value=None): if view: user_id = view.current_user.id role = view.current_role else: user_id = None role = None return cls.new(user_id, role, POST_TYPES.USER, changed_user.id, changed_user.id, MANAGE_OPERATION.USER_REPUTE_CHANGE, value, note=note) @classmethod def add_by_repute_changed_sys(cls, changed_user, note=None, *, value=None): return cls.add_by_repute_changed(None, changed_user, note, value=value) @classmethod def add_by_exp_changed(cls, view, changed_user, note=None, *, value=None): if view: user_id = view.current_user.id role = view.current_role else: user_id = None role = None return cls.new(user_id, role, POST_TYPES.USER, changed_user.id, changed_user.id, MANAGE_OPERATION.USER_EXP_CHANGE, value, note=note) @classmethod def add_by_exp_changed_sys(cls, changed_user, note=None, *, value=None): return cls.add_by_exp_changed(None, changed_user, note, value=value) @classmethod def add_by_post_changed(cls, view, key, operation, related_type, values, old_record, record, note=None, *, value=NotImplemented): def get_val(r, k): if isinstance(r, (DataRecord, dict)): return r[k] elif isinstance(r, BaseModel): return getattr(r, k) else: raise TypeError() def key_in_values(): if isinstance(values, dict): return key in values elif isinstance(values, bool): return values else: raise TypeError() if key_in_values(): if value is NotImplemented: value = [get_val(old_record, key), get_val(record, key)] return cls.new(view.current_user.id, view.current_role, related_type, get_val(record, 'id'), get_val(record, 'user_id'), operation, value, note=note) class Meta: db_table = 'manage_log'
class ManageLog(BaseModel): id = BlobField(primary_key=True) # 使用长ID user_id = BlobField(index=True, null=True) # 操作用户 role = TextField(null=True) # 操作身份 time = MyTimestampField(index=True) # 操作时间 related_type = IntegerField() # 被操作对象类型 related_id = BlobField(index=True) # 被操作对象 related_user_id = BlobField(index=True, null=True) # 被操作对象涉及用户 operation = IntegerField() # 操作行为 value = BinaryJSONField(dumps=json_ex_dumps, null=True) # 操作数据 note = TextField(null=True, default=None) @classmethod def new(cls, user_id, role, related_type, related_id, related_user_id, operation, value, note=None): return cls.create( id=config.LONG_ID_GENERATOR().digest(), user_id=user_id, role=role, time=int(time.time()), related_type=related_type, related_id=related_id, related_user_id=related_user_id, operation=operation, value=value, note=note ) @classmethod def post_new_base(cls, user_id, role, post_type, post_record: DataRecord): """ 新建post,要注意的是这并非是只有管理员能做的操作,因此多数post不计入其中。 只有wiki和board是管理员创建的,予以计入。 :param user_id: :param role: :param post_type: :param post_record: :return: """ title = get_title_by_record(post_type, post_record) return ManageLog.new(user_id, role, post_type, post_record['id'], post_record['user_id'], MOP.POST_CREATE, {'title': title}) @classmethod def post_new(cls, view, post_type, post_record: DataRecord): user_id, role = _get_info(view) return cls.post_new_base(user_id, role, post_type, post_record) @classmethod def add_by_resource_changed(cls, field, op, view, related_user_id, old, new, *, related_type, related_id, note=None): def func(info): info['related_type'] = related_type info['related_id'] = related_id info['related_user_id'] = related_user_id ret = cls.add_by_post_changed(view, field, op, POST_TYPES.USER, True, value={'change': [old, new]}, note=note, cb=func) return ret add_by_credit_changed = _gen_add_by_resource_changed('credit', MOP.USER_CREDIT_CHANGE) add_by_repute_changed = _gen_add_by_resource_changed('repute', MOP.USER_REPUTE_CHANGE) add_by_exp_changed = _gen_add_by_resource_changed('exp', MOP.USER_EXP_CHANGE) @classmethod def add_by_credit_changed_sys(cls, related_user_id, old, new, *, note=None): return cls.add_by_credit_changed(None, related_user_id, old, new, note=note, related_type=POST_TYPES.USER, related_id=related_user_id) @classmethod def add_by_repute_changed_sys(cls, related_user_id, old, new, *, note=None): return cls.add_by_repute_changed(None, related_user_id, old, new, note=note, related_type=POST_TYPES.USER, related_id=related_user_id) @classmethod def add_by_exp_changed_sys(cls, related_user_id, old, new, *, note=None): return cls.add_by_exp_changed(None, related_user_id, old, new, note=note, related_type=POST_TYPES.USER, related_id=related_user_id) @classmethod def add_by_post_changed_base(cls, user_id, role, key, operation, related_type, update_values, old_record=None, record=None, *, note=None, value=NotImplemented, diff_func=save_couple, cb=None): """ 如果指定的列发生了改变,那么新增一条记录,反之什么也不做 :param user_id: 用户 :param role: 角色 :param key: 指定的列名 :param operation: 如果条件达成记录的操作 :param related_type: 被改变的对象的类型 :param update_values: 被提交上来的值,默认为字典,在其中检查key是否存在。若为bool则直接替代key存在与否的判定结果 :param old_record: 应用改动前的值 :param record: 应用改动后的值 :param note: 备注信息 :param value: 默认值为NotImplemented,实现为diff_func的返回结果,若修改则记为想要的值 :param diff_func: 默认值为save_couple,实现为记录前后的变化[old_record[key], record[key]] :param cb: 写入前提供一次修改值的机会 :return: """ def get_val(r, k): if r is None: return if isinstance(r, (DataRecord, dict)): return r[k] elif isinstance(r, BaseModel): return getattr(r, k) else: raise TypeError() def key_in_values(): if isinstance(update_values, dict): return key in update_values elif isinstance(update_values, bool): return update_values else: raise TypeError() if key_in_values(): if value is NotImplemented: old, new = get_val(old_record, key), get_val(record, key) if old == new: return # 修改前后无变化 value = {'change': diff_func(old, new)} info = { 'related_type': related_type, 'related_id': get_val(record, 'id'), 'related_user_id': get_val(record, 'user_id'), 'value': value } if cb: cb(info) if info['related_id'] is None: raise ValueError('未指定 related_id') return cls.new(user_id, role, info['related_type'], info['related_id'], info['related_user_id'], operation, info['value'], note=note) @classmethod def add_by_post_changed(cls, view, key, operation, related_type, update_values, old_record=None, record=None, note=None, *, value=NotImplemented, diff_func=save_couple, cb=None): user_id, role = _get_info(view) return cls.add_by_post_changed_base(user_id, role, key, operation, related_type, update_values, old_record, record, note=note, value=value, diff_func=diff_func, cb=cb) class Meta: db_table = 'manage_log'
class User(PostModel, BaseUser): email = TextField(index=True, unique=True, null=True, default=None) phone = TextField(index=True, unique=True, null=True, default=None) # 大陆地区 nickname = CITextField(index=True, unique=True, null=True) # CITextField password = BlobField() salt = BlobField() # auto biology = TextField(null=True) # 简介 avatar = TextField(null=True) type = IntegerField(default=0) # 账户类型,0默认,1组织 url = TextField(null=True) # 个人主页 location = TextField(null=True) # 所在地 # level = IntegerField(index=True) # 用户级别 group = IntegerField(index=True, default=USER_GROUP.NORMAL) # 用户权限组 is_wiki_editor = BooleanField(default=False, index=True) # 是否为wiki编辑 is_board_moderator = BooleanField(default=False, index=True) # 是否为版主 is_forum_master = BooleanField(default=False, index=True) # 超版 key = BlobField(index=True, null=True) key_time = MyTimestampField() # 最后登录时间 access_time = MyTimestampField(null=True, default=None) # 最后访问时间,以misc为准吧 last_check_in_time = MyTimestampField(null=True, default=None) # 上次签到时间 check_in_his = IntegerField(default=0) # 连续签到天数 number = SerialField( ) # 序号,第N个用户,注意这有个问题是不能写sequence='user_count_seq',应该是peewee的bug credit = IntegerField(default=0) # 积分,会消费 exp = IntegerField(default=0) # 经验值,不会消失 repute = IntegerField(default=0) # 声望 ip_registered = INETField(default=None, null=True) # 注册IP # ref_github = TextField(null=True) # ref_zhihu = TextField(null=True) # ref_weibo = TextField(null=True) is_new_user = BooleanField(default=True) # 是否全新用户(刚注册,未经过修改昵称) phone_verified = BooleanField(default=False) # 手机号已确认 change_nickname_chance = IntegerField(default=0) # 改名机会数量 reset_key = BlobField(index=True, null=True, default=None) # 重置密码所用key class Meta: db_table = 'user' #object_type = OBJECT_TYPES.USER @classmethod def find_by_nicknames(cls, names): it = iter(names) try: condition = cls.nickname == next(it) except StopIteration: return [] for i in it: condition |= cls.nickname == i return cls.select().where(condition) @property def roles(self): """ 这里角色权限由低到高 :return: """ ret = [None] if self.state == POST_STATE.DEL: return ret if self.group >= USER_GROUP.BAN: ret.append('banned_user') if self.group >= USER_GROUP.INACTIVE: ret.append('inactive_user') if self.group >= USER_GROUP.NORMAL: ret.append('user') if self.is_wiki_editor: ret.append('wiki_editor') if self.is_board_moderator: ret.append('board_moderator') if self.is_forum_master: ret.append('forum_master') if self.group >= USER_GROUP.SUPERUSER: ret.append('superuser') if self.group >= USER_GROUP.ADMIN: ret.append('admin') return ret @classmethod def gen_id(cls): return config.POST_ID_GENERATOR() @classmethod def gen_password_and_salt(cls, password_text): salt = os.urandom(32) dk = hashlib.pbkdf2_hmac( config.PASSWORD_SECURE_HASH_FUNC_NAME, password_text.encode('utf-8'), salt, config.PASSWORD_SECURE_HASH_ITERATIONS, ) return {'password': dk, 'salt': salt} @classmethod def gen_key(cls): key = os.urandom(16) key_time = int(time.time()) return {'key': key, 'key_time': key_time} def refresh_key(self): count = 0 while count < 10: with db.atomic(): try: k = self.gen_key() self.key = k['key'] self.key_time = k['key_time'] self.save() return k except DatabaseError: count += 1 db.rollback() raise ValueError("generate key failed") @classmethod def get_by_key(cls, key): try: return cls.get(cls.key == key) except DoesNotExist: return None @classmethod async def gen_reg_code_by_email(cls, email: str, password: str): t = int(time.time()) code = os.urandom(8) email = email.encode('utf-8') pipe = redis.pipeline() pipe.set(RK_USER_REG_CODE_AVAILABLE_TIMES_BY_EMAIL % email, config.USER_REG_CODE_AVAILABLE_TIMES_BY_EMAIL, expire=config.USER_REG_CODE_EXPIRE) pipe.set(RK_USER_REG_CODE_BY_EMAIL % email, code, expire=config.USER_REG_CODE_EXPIRE) pipe.set(RK_USER_REG_PASSWORD % email, password, expire=config.USER_REG_CODE_EXPIRE) await pipe.execute() return code @classmethod async def reg_code_cleanup(cls, email): email = email.encode('utf-8') pipe = redis.pipeline() pipe.delete(RK_USER_REG_CODE_BY_EMAIL % email) pipe.delete(RK_USER_REG_CODE_AVAILABLE_TIMES_BY_EMAIL % email) pipe.delete(RK_USER_REG_PASSWORD % email) await pipe.execute() @classmethod async def check_reg_code_by_email(cls, email, code): """ 检查账户激活码是否可用 :param uid: :param code: :return: """ if not code: return if isinstance(code, str): code = to_bin(code) if len(code) == 8: email_bytes = email.encode('utf-8') rk_code = RK_USER_REG_CODE_BY_EMAIL % email_bytes rk_times = RK_USER_REG_CODE_AVAILABLE_TIMES_BY_EMAIL % email_bytes rk_pw = RK_USER_REG_PASSWORD % email_bytes if await redis.get(rk_code) == code: # 检查可用次数,decr的返回值是执行后的 if int(await redis.decr(rk_times)) <= 0: return await cls.reg_code_cleanup(email) # 无问题,取出储存值 return (await redis.get(rk_pw)).decode('utf-8') async def can_request_reset_password(self): """ 是否能申请重置密码 :return: """ val = await redis.get(RK_USER_LAST_REQUEST_RESET_KEY_BY_USER_ID % self.id) if val is None: return True if time.time() - int(val) > config.USER_RESET_PASSWORD_CODE_EXPIRE: return True def gen_reset_key(self) -> bytes: """ 生成一个重置密码key :return: """ # len == 16 + 8 == 24 t = int(time.time()) code = os.urandom(16) + t.to_bytes(8, 'little') redis.set(RK_USER_LAST_REQUEST_RESET_KEY_BY_USER_ID % self.id, t, expire=config.USER_RESET_PASSWORD_REQUST_INTERVAL) redis.set(RK_USER_RESET_KEY_BY_USER_ID % self.id, code, expire=config.USER_RESET_PASSWORD_CODE_EXPIRE) return code @classmethod async def check_reset_key(cls, uid, code) -> Union['User', None]: """ 检查uid与code这一组密码重置密钥是否有效 :param uid: :param code: :return: """ if not code: return if isinstance(uid, str): uid = to_bin(uid) if isinstance(code, str): code = to_bin(code) if len(code) == 24: rkey = RK_USER_RESET_KEY_BY_USER_ID % uid if await redis.get(rkey) == code: try: u = cls.get(cls.id == uid) await redis.delete(rkey) return u except cls.DoesNotExist: pass def set_password(self, new_password): info = self.gen_password_and_salt(new_password) self.salt = info['salt'] self.password = info['password'] self.save() def update_access_time(self): self.access_time = int(time.time()) self.save() return self.access_time def check_in(self): self.last_check_in_time = self.last_check_in_time or 0 old_time = self.last_check_in_time last_midnight = get_today_start_timestamp() # 今日未签到 if self.last_check_in_time < last_midnight: self.last_check_in_time = int(time.time()) # 三天内有签到,连击 if old_time > last_midnight - config.USER_CHECKIN_COUNTER_INTERVAL: self.check_in_his += 1 else: self.check_in_his = 1 self.save() # 签到加分 credit = self.credit exp = self.exp self.credit += 5 self.exp += 5 self.save() ManageLog.add_by_credit_changed_sys(self.id.tobytes(), credit, self.credit, note='每日签到') ManageLog.add_by_exp_changed_sys(self.id.tobytes(), exp, self.exp, note='每日签到') return { 'credit': 5, 'exp': 5, 'time': self.last_check_in_time, 'check_in_his': self.check_in_his } def daily_access_reward(self): self.access_time = self.access_time or 0 old_time = self.access_time self.update_access_time() if old_time < get_today_start_timestamp(): exp = self.exp self.exp += 5 self.save() ManageLog.add_by_exp_changed_sys(self.id.tobytes(), exp, self.exp, note='每日登录') return 5 def _auth_base(self, password_text): """ 已获取了用户对象,进行密码校验 :param password_text: :return: """ dk = hashlib.pbkdf2_hmac( config.PASSWORD_SECURE_HASH_FUNC_NAME, password_text.encode('utf-8'), self.salt.tobytes(), config.PASSWORD_SECURE_HASH_ITERATIONS, ) if self.password.tobytes() == dk: return self @classmethod def auth_by_mail(cls, email, password_text): try: u = cls.get(cls.email == email) except DoesNotExist: return False return u._auth_base(password_text) @classmethod def auth_by_nickname(cls, nickname, password_text): try: u = cls.get(cls.nickname == nickname) except DoesNotExist: return False return u._auth_base(password_text) def __repr__(self): return '<User id:%x nickname:%r>' % (int.from_bytes( self.id.tobytes(), 'big'), self.nickname) @classmethod def get_post_type(cls): return POST_TYPES.USER def get_title(self): return self.nickname
class User(PostModel, BaseUser): email = TextField(index=True, unique=True) nickname = CITextField(index=True, unique=True) # CITextField password = BlobField() salt = BlobField() # auto biology = TextField(null=True) # 简介 avatar = TextField(null=True) type = IntegerField(default=0) # 账户类型,0默认,1组织 url = TextField(null=True) # 个人主页 location = TextField(null=True) # 所在地 # level = IntegerField(index=True) # 用户级别 group = IntegerField(index=True) # 用户权限组 key = BlobField(index=True, null=True) key_time = MyTimestampField() # 最后登录时间 access_time = MyTimestampField(null=True, default=None) # 最后访问时间,以misc为准吧 last_check_in_time = MyTimestampField(null=True, default=None) # 上次签到时间 check_in_his = IntegerField(default=0) # 连续签到天数 phone = TextField(null=True, default=None) # 大陆地区 number = IntegerField(default=get_user_count_seq) # 序号,第N个用户 sequence='user_count_seq' credit = IntegerField(default=0) # 积分,会消费 reputation = IntegerField(default=0) # 声望,不会消失 reset_key = BlobField(index=True, null=True, default=None) # 重置密码所用key class Meta: db_table = 'user' #object_type = OBJECT_TYPES.USER @property def roles(self): ret = [None] if self.group >= USER_GROUP.ADMIN: ret.append('admin') if self.group >= USER_GROUP.SUPERUSER: ret.append('superuser') if self.group >= USER_GROUP.NORMAL: ret.append('user') if self.group >= USER_GROUP.INACTIVE: ret.append('inactive_user') return ret @classmethod def gen_id(cls): return config.POST_ID_GENERATOR() @classmethod def gen_password_and_salt(cls, password_text): salt = os.urandom(16) m = hmac.new(salt, digestmod=config.PASSWORD_HASH_FUNC) m.update(password_text.encode('utf-8')) return {'password': m.digest(), 'salt': salt} @classmethod def gen_key(cls): key = os.urandom(16) key_time = int(time.time()) return {'key': key, 'key_time': key_time} def refresh_key(self): count = 0 while count < 10: with db.atomic(): try: k = self.gen_key() self.key = k['key'] self.key_time = k['key_time'] self.save() return k except DatabaseError: count += 1 db.rollback() raise ValueError("generate key failed") @classmethod def get_by_key(cls, key): try: return cls.get(cls.key == key) except DoesNotExist: return None def get_activation_code(self): raw = self.salt.tobytes() + self.time.to_bytes(8, 'little') # len == 16 + 8 == 24 return str(binascii.hexlify(raw), 'utf-8') @classmethod def check_active(cls, uid, code): if not code: return try: uid = binascii.unhexlify(uid) code = binascii.unhexlify(code) except: return if len(code) == 24: # 时间为最近3天 ts = int.from_bytes(binascii.unhexlify(code[16:]), 'little') if time.time() - ts < 3 * 24 * 60 * 60: try: u = cls.get(cls.time == ts, cls.id == uid, cls.group == USER_GROUP.INACTIVE, cls.salt == binascii.unhexlify(code[:16])) u.group = USER_GROUP.NORMAL u.save() return u except cls.DoesNotExist: pass @staticmethod def gen_reset_key(): return os.urandom(16) + int(time.time()).to_bytes(8, 'little') # len == 16 + 8 == 24 @classmethod def check_reset(cls, uid, code) -> Union['User', None]: if not code: return try: uid = binascii.unhexlify(uid) code = binascii.unhexlify(code) except: return if len(code) == 24: # 时间为最近12小时 ts = int.from_bytes(code[16:], 'little') if time.time() - ts < 12 * 60 * 60: try: u = cls.get(cls.id == uid, cls.reset_key == code) return u except cls.DoesNotExist: pass def set_password(self, new_password): info = self.gen_password_and_salt(new_password) self.salt = info['salt'] self.password = info['password'] self.save() def update_access_time(self): self.access_time = int(time.time()) self.save() return self.access_time def check_in(self): self.last_check_in_time = self.last_check_in_time or 0 old_time = self.last_check_in_time last_midnight = get_today_start_timestamp() # 今日未签到 if self.last_check_in_time < last_midnight: self.last_check_in_time = int(time.time()) # 三天内有签到,连击 print(old_time, last_midnight - 3 * 24 * 60 * 60) if old_time > last_midnight - 3 * 24 * 60 * 60: self.check_in_his += 1 else: self.check_in_his = 1 self.save() # 签到加分 credit = self.credit reputation = self.reputation self.credit += 5 self.reputation += 5 self.save() ManageLog.add_by_credit_changed_sys(self, note='每日签到', value=[credit, self.credit]) ManageLog.add_by_reputation_changed_sys(self, note='每日签到', value=[reputation, self.reputation]) return { 'credit': 5, 'reputation': 5, 'time': self.last_check_in_time, 'check_in_his': self.check_in_his } def daily_access_reward(self): self.access_time = self.access_time or 0 old_time = self.access_time self.update_access_time() if old_time < get_today_start_timestamp(): credit = self.credit self.credit += 5 self.save() ManageLog.add_by_credit_changed_sys(self, note='每日登录', value=[credit, self.credit]) return 5 @classmethod def auth(cls, email, password_text): try: u = cls.get(cls.email == email) except DoesNotExist: return False m = hmac.new(u.salt.tobytes(), digestmod=config.PASSWORD_HASH_FUNC) m.update(password_text.encode('utf-8')) if u.password.tobytes() == m.digest(): return u def __repr__(self): return '<User id:%x nickname:%r>' % (int.from_bytes(self.id.tobytes(), 'big'), self.nickname)
class Notification(BaseModel): id = BlobField(primary_key=True) type = IntegerField(index=True) # 行为 time = MyTimestampField(index=True) # 行为发生时间 loc_post_type = IntegerField() # 地点,类型 loc_post_id = BlobField() # 地点 loc_post_title = TextField(null=True, default=None) # 地点标题 sender_ids = ArrayField(BlobField) # 人物,行为方 receiver_id = BlobField(index=True) # 人物,被动方 from_post_type = IntegerField(null=True) # 信息来源,例如A在B帖回复@C,来源是@创建的那个提醒对象,此时related指向那条回复 from_post_id = BlobField(null=True) related_type = IntegerField(null=True, default=None) # 可选,关联类型 related_id = BlobField(null=True, default=None) # 可选,关联ID。例如A在B帖回复C,人物是A和C,地点是B,关联是这个回复的ID brief = TextField(null=True, default=None) # 一小段预览,可有可无 data = BinaryJSONField(dumps=json_ex_dumps, null=True, default=None) # 附加数据 is_read = BooleanField(default=False) @classmethod def count(cls, user_id): return cls.select().where(cls.receiver_id == user_id, cls.is_read == False).count() @classmethod def set_read(cls, user_id): cur = db.execute_sql(''' WITH updated_rows as ( UPDATE notif SET is_read = TRUE WHERE "receiver_id" = %s AND "is_read" = FALSE RETURNING is_read ) SELECT count(is_read) FROM updated_rows; ''', (user_id,)) return cur.fetchone()[0] @classmethod def refresh(cls, user_id, cooldown = config.NOTIF_FETCH_COOLDOWN): new = [] r: UserNotifLastInfo = UserNotifLastInfo.get_by_pk(user_id) if not r: return if cooldown and (time.time() - r.update_time < cooldown): return def pack_notif(i: Dict): i.update({ 'id': config.LONG_ID_GENERATOR().to_bin() }) # 注意,insert_many 要求所有列一致,因此不能出现有的数据独有a列,有的数据独有b列 # 要都有才行,因此全部进行默认填充 i.setdefault('related_type', None) i.setdefault('related_id', None) i.setdefault('brief', None) i.setdefault('data', None) return i newlst = r.get_notifications(True) newlst.sort(key=lambda x: x['time'], reverse=True) newlst = list(map(pack_notif, newlst)) if newlst: cls.insert_many(newlst).execute() return len(newlst) class Meta: db_table = 'notif'