diff --git a/hole.py b/hole.py index 0ddf018..ead997d 100644 --- a/hole.py +++ b/hole.py @@ -10,7 +10,7 @@ from sqlalchemy.sql.expression import func from mastodon import Mastodon from models import db, User, Post, Comment, Attention, TagRecord, Syslog -from utils import get_current_username, map_post, map_comment, map_syslog, check_attention, hash_name, look, get_num, tmp_token, is_admin, check_can_del, rds, RDS_KEY_POLL_OPTS, RDS_KEY_POLL_VOTES, gen_poll_dict, name_with_tmp_limit +from utils import get_current_username, map_post, map_comment, map_syslog, check_attention, hash_name, look, get_num, tmp_token, is_admin, check_can_del, rds, RDS_KEY_POLL_OPTS, RDS_KEY_POLL_VOTES, gen_poll_dict, name_with_tmp_limit, RDS_KEY_BLOCK_SET, RDS_KEY_BLOCKED_COUNT, RDS_KEY_DANGEROUS_USERS app = Flask(__name__) app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///hole.db' @@ -39,6 +39,10 @@ limiter = Limiter( ) PER_PAGE = 50 +DANGEROUS_USER_THRESHOLD = 10 + +# 重置后旧的被拉黑次数可以丢弃了,但其他仍需要保留 +rds.delete(RDS_KEY_BLOCKED_COUNT) class APIError(Exception): @@ -558,5 +562,45 @@ def add_vote(): } +@app.route('/_api/v1/block', methods=['POST']) +@limiter.limit("15 / hour; 1 / 2 second") +def block_user_by_target(): + username = get_current_username() + target_type = request.form.get('type') + target_id = request.form.get('id', type=int) + + if username.startswith('tmp_'): + raise APIError('临时用户无法拉黑') + + if target_type == 'post': + target = Post.query.get_or_404(target_id) + elif target_type == 'comment': + target = Comment.query.get_or_404(target_id) + else: + raise APIError('无效的type') + + if hash_name(username) == target.name_hash: + raise APIError('不可拉黑自己') + + if is_admin(username): + rds.sadd(RDS_KEY_DANGEROUS_USERS, target.name_hash) + curr_cnt = rds.hget(RDS_KEY_BLOCKED_COUNT, target.name_hash) + else: + if rds.sismember(RDS_KEY_BLOCK_SET % username, target.name_hash): + raise APIError('已经拉黑了') + rds.sadd(RDS_KEY_BLOCK_SET % username, target.name_hash) + curr_cnt = rds.hincrby(RDS_KEY_BLOCKED_COUNT, target.name_hash, 1) + if curr_cnt >= DANGEROUS_USER_THRESHOLD: + rds.sadd(RDS_KEY_DANGEROUS_USERS, target.name_hash) + + return { + 'code': 0, + 'data': { + 'curr': curr_cnt, + 'threshold': DANGEROUS_USER_THRESHOLD + } + } + + if __name__ == '__main__': app.run(debug=True) diff --git a/utils.py b/utils.py index 5f84e53..2305f3d 100644 --- a/utils.py +++ b/utils.py @@ -10,6 +10,10 @@ from config import RDS_CONFIG, ADMINS, ENABLE_TMP RDS_KEY_POLL_OPTS = 'hole_thu:poll_opts:%s' RDS_KEY_POLL_VOTES = 'hole_thu:poll_votes:%s:%s' +RDS_KEY_BLOCK_SET = 'hole_thu:block_list:%s' # key的参数是name而非namehash,为了方便清理和持续拉黑。拉黑名单不那么敏感,应该可以接受后台实名。value是namehash。 +RDS_KEY_BLOCKED_COUNT = 'hole_thu:blocked_count' # namehash -> 被拉黑次数 +RDS_KEY_DANGEROUS_USERS = 'hole_thu:dangerous_users' + rds = redis.Redis(**RDS_CONFIG) @@ -52,11 +56,14 @@ def hash_name(name): def map_post(p, name, mc=50): + blocked = is_blocked(p.name_hash, name) + # TODO: 如果未来量大还是sql里not in一下 r = { + 'blocked': blocked, 'pid': p.id, 'likenum': p.likenum, 'cw': p.cw, - 'text': p.content, + 'text': '' if blocked else p.content, 'timestamp': p.timestamp, 'type': p.post_type, 'url': p.file_url, @@ -65,7 +72,7 @@ def map_post(p, name, mc=50): 'attention': check_attention(name, p.id), 'can_del': check_can_del(name, p.name_hash), 'allow_search': bool(p.search_text), - 'poll': gen_poll_dict(p.id, name) + 'poll': None if blocked else gen_poll_dict(p.id, name) } if is_admin(name): r['hot_score'] = p.hot_score @@ -98,6 +105,19 @@ def name_with_tmp_limit(name: str) -> str: 'tmp_') else name +def is_blocked(target_name_hash, name): + if rds.sismember(RDS_KEY_BLOCK_SET % name, target_name_hash): + return True + if rds.sismember( + RDS_KEY_DANGEROUS_USERS, target_name_hash + ) and not ( + is_admin(name) or rds.sismember( + RDS_KEY_DANGEROUS_USERS, hash_name(name)) + ): + return True + return False + + def map_comment(p, name): names = {p.name_hash: 0} @@ -108,10 +128,11 @@ def map_comment(p, name): return names[nh] return [{ + 'blocked': (blocked := is_blocked(c.name_hash, name)), 'cid': c.id, 'name_id': gen_name_id(c.name_hash), 'pid': p.id, - 'text': c.content, + 'text': '' if blocked else c.content, 'timestamp': c.timestamp, 'can_del': check_can_del(name, c.name_hash) } for c in p.comments if not (c.deleted and gen_name_id(c.name_hash) >= 0)