diff --git a/.gitignore b/.gitignore index 9e37a3a..b9256e9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ +/migrations/ + __pycache__/ *.pyc *.db diff --git a/hole.py b/hole.py index ad4432e..98af0be 100644 --- a/hole.py +++ b/hole.py @@ -6,10 +6,11 @@ from flask import Flask, request, abort, redirect from flask_limiter import Limiter from flask_limiter.util import get_remote_address from flask_migrate import Migrate +from sqlalchemy.sql.expression import func from mastodon import Mastodon from models import db, User, Post, Comment, Attention, TagRecord, Syslog -from utils import require_token, map_post, map_comment, map_syslog, check_attention, hash_name, look, get_num, tmp_token +from utils import get_current_user, map_post, map_comment, map_syslog, check_attention, hash_name, look, get_num, tmp_token, is_admin app = Flask(__name__) app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///hole.db' @@ -44,7 +45,7 @@ def login(): if provider == 'cs': return redirect(CS_LOGIN_URL) - abort(404) + abort(401) @app.route('/_auth') @@ -85,17 +86,30 @@ def auth(): @app.route('/_api/v1/getlist') def get_list(): - u = require_token() + u = get_current_user() - p = get_num(request.args.get('p')) + p = request.args.get('p', type=int, default=1) + order_mode = request.args.get('order_mode', type=int, default=0) + if request.args.get('by_c'): + order_mode = 1 # 兼容旧版前端 + if order_mode == 3: + p = 1 - posts = Post.query.filter_by(deleted=False) + query = Post.query.filter_by(deleted=False) if 'no_cw' in request.args: - posts = posts.filter_by(cw=None) - posts = posts.order_by( - db.desc('comment_timestamp')) if 'by_c' in request.args else posts.order_by( - db.desc('id')) - posts = posts.paginate(p, PER_PAGE) + query = query.filter_by(cw=None) + if order_mode == 2: + query = query.filter( + Post.hot_score != -1 + ).filter_by(is_reported=False) + + order = { + 1: Post.comment_timestamp.desc(), # 最近评论 + 2: Post.hot_score.desc(), # 热门 + 3: func.random() # 随机 + }.get(order_mode, Post.id.desc()) # 最新 + + posts = query.order_by(order).paginate(p, PER_PAGE) data = list(map(map_post, posts.items, [u.name] * len(posts.items))) @@ -109,13 +123,11 @@ def get_list(): @app.route('/_api/v1/getone') def get_one(): - u = require_token() + u = get_current_user() pid = request.args.get('pid', type=int) - post = Post.query.get(pid) - if not post: - abort(404) + post = Post.query.get_or_404(pid) if post.deleted or post.is_reported: abort(451) @@ -129,7 +141,7 @@ def get_one(): @app.route('/_api/v1/search') def search(): - u = require_token() + u = get_current_user() page = request.args.get('page', type=int, default=1) pagesize = min(request.args.get('pagesize', type=int, default=200), 200) @@ -179,7 +191,7 @@ def search(): @app.route('/_api/v1/dopost', methods=['POST']) @limiter.limit("50 / hour; 1 / 3 second") def do_post(): - u = require_token() + u = get_current_user() allow_search = request.form.get('allow_search') print(allow_search) @@ -238,7 +250,7 @@ def do_post(): @app.route('/_api/v1/editcw', methods=['POST']) @limiter.limit("50 / hour; 1 / 2 second") def edit_cw(): - u = require_token() + u = get_current_user() cw = request.form.get('cw') pid = get_num(request.form.get('pid')) @@ -247,9 +259,7 @@ def edit_cw(): if cw and len(cw) > 32: abort(422) - post = Post.query.get(pid) - if not post: - abort(404) + post = Post.query.get_or_404(pid) if post.deleted: abort(451) @@ -265,7 +275,7 @@ def edit_cw(): @app.route('/_api/v1/getcomment') def get_comment(): - u = require_token() + u = get_current_user() pid = get_num(request.args.get('pid')) @@ -288,7 +298,7 @@ def get_comment(): @app.route('/_api/v1/docomment', methods=['POST']) @limiter.limit("50 / hour; 1 / 3 second") def do_comment(): - u = require_token() + u = get_current_user() pid = get_num(request.form.get('pid')) @@ -311,6 +321,9 @@ def do_comment(): post.comments.append(c) post.comment_timestamp = c.timestamp + if post.hot_score != -1: + post.hot_score += 1 + at = Attention.query.filter_by( name_hash=hash_name(u.name), pid=pid ).first() @@ -319,6 +332,8 @@ def do_comment(): at = Attention(name_hash=hash_name(u.name), pid=pid, disabled=False) db.session.add(at) post.likenum += 1 + if post.hot_score != -1: + post.hot_score += 2 else: if at.disabled: post.likenum += 1 @@ -335,7 +350,7 @@ def do_comment(): @app.route('/_api/v1/attention', methods=['POST']) @limiter.limit("200 / hour; 1 / second") def attention(): - u = require_token() + u = get_current_user() if u.name[:4] == 'tmp_': abort(403) @@ -356,11 +371,14 @@ def attention(): if not at: at = Attention(name_hash=hash_name(u.name), pid=pid, disabled=True) db.session.add(at) + if post.hot_score != -1: + post.hot_score += 2 if(at.disabled != (s == '0')): at.disabled = (s == '0') post.likenum += 1 - 2 * int(s == '0') - db.session.commit() + + db.session.commit() return { 'code': 0, @@ -371,7 +389,7 @@ def attention(): @app.route('/_api/v1/getattention') def get_attention(): - u = require_token() + u = get_current_user() ats = Attention.query.with_entities( Attention.pid @@ -401,7 +419,7 @@ def get_attention(): @app.route('/_api/v1/delete', methods=['POST']) @limiter.limit("50 / hour; 1 / 3 second") def delete(): - u = require_token() + u = get_current_user() obj_type = request.form.get('type') obj_id = get_num(request.form.get('id')) @@ -425,11 +443,6 @@ def delete(): Attention.query.filter_by(pid=obj.id).delete() TagRecord.query.filter_by(pid=obj.id).delete() db.session.delete(obj) - db.session.add(Syslog( - log_type='SELF DELETE POST', - log_detail=f"pid={obj_id}\n{note}", - name_hash=hash_name(u.name) - )) else: obj.deleted = True elif u.name in app.config.get('ADMINS'): @@ -454,7 +467,7 @@ def delete(): @app.route('/_api/v1/systemlog') def system_log(): - u = require_token() + u = get_current_user() ss = Syslog.query.order_by(db.desc('timestamp')).limit(100).all() @@ -462,14 +475,14 @@ def system_log(): 'start_time': app.config['START_TIME'], 'salt': look(app.config['SALT']), 'tmp_token': tmp_token(), - 'data': [map_syslog(s,u) for s in ss] + 'data': [map_syslog(s, u) for s in ss] } @app.route('/_api/v1/report', methods=['POST']) @limiter.limit("10 / hour; 1 / 3 second") def report(): - u = require_token() + u = get_current_user() pid = get_num(request.form.get('pid')) @@ -490,5 +503,22 @@ def report(): return {'code': 0} +@app.route('/_api/v1/update_score', methods=['POST']) +def edit_hot_score(): + u = get_current_user() + if not is_admin(u.name): + print(u.name) + abort(403) + + pid = request.form.get('pid', type=int) + score = request.form.get('score', type=int) + + post = Post.query.get_or_404(pid) + post.hot_score = score + db.session.commit() + + return {'code': 0} + + if __name__ == '__main__': app.run(debug=True) diff --git a/migrations/README b/migrations/README deleted file mode 100644 index 98e4f9c..0000000 --- a/migrations/README +++ /dev/null @@ -1 +0,0 @@ -Generic single-database configuration. \ No newline at end of file diff --git a/migrations/alembic.ini b/migrations/alembic.ini deleted file mode 100644 index f8ed480..0000000 --- a/migrations/alembic.ini +++ /dev/null @@ -1,45 +0,0 @@ -# A generic, single database configuration. - -[alembic] -# template used to generate migration files -# file_template = %%(rev)s_%%(slug)s - -# set to 'true' to run the environment during -# the 'revision' command, regardless of autogenerate -# revision_environment = false - - -# Logging configuration -[loggers] -keys = root,sqlalchemy,alembic - -[handlers] -keys = console - -[formatters] -keys = generic - -[logger_root] -level = WARN -handlers = console -qualname = - -[logger_sqlalchemy] -level = WARN -handlers = -qualname = sqlalchemy.engine - -[logger_alembic] -level = INFO -handlers = -qualname = alembic - -[handler_console] -class = StreamHandler -args = (sys.stderr,) -level = NOTSET -formatter = generic - -[formatter_generic] -format = %(levelname)-5.5s [%(name)s] %(message)s -datefmt = %H:%M:%S diff --git a/migrations/env.py b/migrations/env.py deleted file mode 100644 index 9452179..0000000 --- a/migrations/env.py +++ /dev/null @@ -1,96 +0,0 @@ -from __future__ import with_statement - -import logging -from logging.config import fileConfig - -from sqlalchemy import engine_from_config -from sqlalchemy import pool - -from alembic import context - -# this is the Alembic Config object, which provides -# access to the values within the .ini file in use. -config = context.config - -# Interpret the config file for Python logging. -# This line sets up loggers basically. -fileConfig(config.config_file_name) -logger = logging.getLogger('alembic.env') - -# add your model's MetaData object here -# for 'autogenerate' support -# from myapp import mymodel -# target_metadata = mymodel.Base.metadata -from flask import current_app -config.set_main_option( - 'sqlalchemy.url', - str(current_app.extensions['migrate'].db.engine.url).replace('%', '%%')) -target_metadata = current_app.extensions['migrate'].db.metadata - -# other values from the config, defined by the needs of env.py, -# can be acquired: -# my_important_option = config.get_main_option("my_important_option") -# ... etc. - - -def run_migrations_offline(): - """Run migrations in 'offline' mode. - - This configures the context with just a URL - and not an Engine, though an Engine is acceptable - here as well. By skipping the Engine creation - we don't even need a DBAPI to be available. - - Calls to context.execute() here emit the given string to the - script output. - - """ - url = config.get_main_option("sqlalchemy.url") - context.configure( - url=url, target_metadata=target_metadata, literal_binds=True - ) - - with context.begin_transaction(): - context.run_migrations() - - -def run_migrations_online(): - """Run migrations in 'online' mode. - - In this scenario we need to create an Engine - and associate a connection with the context. - - """ - - # this callback is used to prevent an auto-migration from being generated - # when there are no changes to the schema - # reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html - def process_revision_directives(context, revision, directives): - if getattr(config.cmd_opts, 'autogenerate', False): - script = directives[0] - if script.upgrade_ops.is_empty(): - directives[:] = [] - logger.info('No changes in schema detected.') - - connectable = engine_from_config( - config.get_section(config.config_ini_section), - prefix='sqlalchemy.', - poolclass=pool.NullPool, - ) - - with connectable.connect() as connection: - context.configure( - connection=connection, - target_metadata=target_metadata, - process_revision_directives=process_revision_directives, - **current_app.extensions['migrate'].configure_args - ) - - with context.begin_transaction(): - context.run_migrations() - - -if context.is_offline_mode(): - run_migrations_offline() -else: - run_migrations_online() diff --git a/migrations/script.py.mako b/migrations/script.py.mako deleted file mode 100644 index 2c01563..0000000 --- a/migrations/script.py.mako +++ /dev/null @@ -1,24 +0,0 @@ -"""${message} - -Revision ID: ${up_revision} -Revises: ${down_revision | comma,n} -Create Date: ${create_date} - -""" -from alembic import op -import sqlalchemy as sa -${imports if imports else ""} - -# revision identifiers, used by Alembic. -revision = ${repr(up_revision)} -down_revision = ${repr(down_revision)} -branch_labels = ${repr(branch_labels)} -depends_on = ${repr(depends_on)} - - -def upgrade(): - ${upgrades if upgrades else "pass"} - - -def downgrade(): - ${downgrades if downgrades else "pass"} diff --git a/migrations/versions/0ad9747d0874_record_timestap_of_the_lastest_comment.py b/migrations/versions/0ad9747d0874_record_timestap_of_the_lastest_comment.py deleted file mode 100644 index 8a3bab6..0000000 --- a/migrations/versions/0ad9747d0874_record_timestap_of_the_lastest_comment.py +++ /dev/null @@ -1,28 +0,0 @@ -"""record timestap of the lastest comment - -Revision ID: 0ad9747d0874 -Revises: -Create Date: 2020-09-10 21:06:17.163526 - -""" -from alembic import op -import sqlalchemy as sa - - -# revision identifiers, used by Alembic. -revision = '0ad9747d0874' -down_revision = None -branch_labels = None -depends_on = None - - -def upgrade(): - # ### commands auto generated by Alembic - please adjust! ### - op.add_column('post', sa.Column('comment_timestamp', sa.Integer(), nullable=True)) - # ### end Alembic commands ### - - -def downgrade(): - # ### commands auto generated by Alembic - please adjust! ### - op.drop_column('post', 'comment_timestamp') - # ### end Alembic commands ### diff --git a/models.py b/models.py index 571a17e..e3adcf1 100644 --- a/models.py +++ b/models.py @@ -25,7 +25,8 @@ class Post(db.Model): timestamp = db.Column(db.Integer) deleted = db.Column(db.Boolean, default=False) is_reported = db.Column(db.Boolean, default=False) - comment_timestamp = db.Column(db.Integer, default=0) + comment_timestamp = db.Column(db.Integer, default=0, index=True) + hot_score = db.Column(db.Integer, default=0, nullable=False, server_default="0") comments = db.relationship('Comment', backref='post', lazy=True) diff --git a/utils.py b/utils.py index 9824d7d..4cd21b8 100644 --- a/utils.py +++ b/utils.py @@ -8,12 +8,17 @@ def get_config(key): return current_app.config.get(key) +def is_admin(name): + return name in get_config('ADMINS') + + def tmp_token(): - return hash_name(str(int(time.time() / 900)) + - User.query.get(1).token)[5:21] + return hash_name( + str(int(time.time() / 900)) + User.query.get(1).token + )[5:21] -def require_token(): +def get_current_user(): token = request.headers.get('User-Token') or request.args.get('user_token') if not token: abort(401) @@ -37,7 +42,7 @@ def hash_name(name): def map_post(p, name, mc=50): - return { + r = { 'pid': p.id, 'likenum': p.likenum, 'cw': p.cw, @@ -51,6 +56,9 @@ def map_post(p, name, mc=50): 'can_del': check_can_del(name, p.name_hash), 'allow_search': bool(p.search_text) } + if is_admin(name): + r['hot_score'] = p.hot_score + return r def map_comment(p, name): @@ -91,8 +99,7 @@ def check_attention(name, pid): def check_can_del(name, author_hash): - return 1 if hash_name( - name) == author_hash or name in get_config('ADMINS') else 0 + return int(hash_name(name) == author_hash or is_admin(name)) def look(s):