示例#1
0
class Statistic24hLog(BaseModel):
    id = BlobField(primary_key=True)
    time = MyTimestampField(index=True)
    data = BinaryJSONField(dumps=json_ex_dumps)

    class Meta:
        db_table = 'statistic24h_log'
示例#2
0
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()
示例#3
0
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'
示例#4
0
文件: follow.py 项目: yisan/Icarus
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'
示例#5
0
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
示例#6
0
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
示例#7
0
文件: wiki.py 项目: zaevi/Icarus
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'
示例#8
0
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
示例#9
0
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'
示例#10
0
文件: notif.py 项目: showsmall/Icarus
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'
示例#11
0
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
示例#12
0
文件: user.py 项目: zhoudaqing/Icarus
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
示例#13
0
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'
示例#14
0
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'
示例#15
0
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'
示例#16
0
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
示例#17
0
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)
示例#18
0
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'