diff --git a/app/__init__.py b/app/__init__.py index 598d51b..d167a80 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -76,11 +76,13 @@ def create_app(): from app.routes.threads import bp as threads_bp from app.routes.users import bp as users_bp from app.routes.mod import bp as mod_bp + from app.routes.api import bp as api_bp app.register_blueprint(app_bp) app.register_blueprint(topics_bp) app.register_blueprint(threads_bp) app.register_blueprint(users_bp) app.register_blueprint(mod_bp) + app.register_blueprint(api_bp) app.config['SESSION_COOKIE_SECURE'] = True @@ -88,11 +90,12 @@ def create_app(): def make_session_permanent(): session.permanent = True + commit = "" + with open('.git/refs/heads/main') as f: + commit = f.read().strip() + @app.context_processor def inject_constants(): - commit = "" - with open('.git/refs/heads/main') as f: - commit = f.read().strip() return { "InfoboxIcons": InfoboxIcons, "InfoboxHTMLClass": InfoboxHTMLClass, diff --git a/app/models.py b/app/models.py index c92bf7a..2c99430 100644 --- a/app/models.py +++ b/app/models.py @@ -1,5 +1,6 @@ from .db import Model, db from .constants import PermissionLevel +import time class Users(Model): table = "users" @@ -219,3 +220,27 @@ class Avatars(Model): class Subscriptions(Model): table = "subscriptions" + +class APIRateLimits(Model): + table = 'api_rate_limits' + + @classmethod + def is_allowed(cls, user_id, method, seconds): + q = """ + SELECT logged_at FROM api_rate_limits + WHERE user_id = ? AND method = ? + ORDER BY logged_at DESC LIMIT 1""" + last_call = db.fetch_one(q, user_id, method) + if last_call is None or (int(time.time()) - int(last_call['logged_at']) >= seconds): + with db.transaction(): + db.query( + 'DELETE FROM api_rate_limits WHERE user_id = ? AND method = ?', + user_id, method + ) + db.query( + 'INSERT INTO api_rate_limits (user_id, method) VALUES (?, ?)', + user_id, method + ) + return True + else: + return False diff --git a/app/routes/api.py b/app/routes/api.py new file mode 100644 index 0000000..6c141ab --- /dev/null +++ b/app/routes/api.py @@ -0,0 +1,43 @@ +from flask import Blueprint, request, url_for +from ..lib.babycode import babycode_to_html +from .users import is_logged_in, get_active_user +from ..models import APIRateLimits, Threads +from ..db import db + +bp = Blueprint("api", __name__, url_prefix="/api/") + + +@bp.post('/thread-updates/') +def thread_updates(thread_id): + thread = Threads.find({'id': thread_id}) + if not thread: + return {'error': 'no such thread'}, 404 + target_time = request.json.get('since') + if not target_time: + return {'error': 'missing parameter "since"'}, 400 + try: + target_time = int(target_time) + except: + return {'error': 'parameter "since" is not/cannot be converted to a number'}, 400 + + q = 'SELECT id FROM posts WHERE thread_id = ? AND posts.created_at > ? ORDER BY posts.created_at ASC LIMIT 1' + new_post = db.fetch_one(q, thread_id, target_time) + if not new_post: + return {'status': 'none'} + + url = url_for('threads.thread', slug=thread.slug, after=new_post['id'], _anchor=f'post-{new_post['id']}') + return {'status': 'new_post', 'url': url} + + +@bp.post('/babycode-preview') +def babycode_preview(): + if not is_logged_in(): + return {'error': 'not authorized'}, 401 + user = get_active_user() + if not APIRateLimits.is_allowed(user.id, 'babycode_preview', 5): + return {'error': 'too many requests'}, 429 + markup = request.json.get('markup') + if not markup or not isinstance(markup, str): + return {'error': 'markup field missing or invalid type'}, 400 + rendered = babycode_to_html(markup) + return {'html': rendered} diff --git a/app/templates/threads/thread.html b/app/templates/threads/thread.html index acf1c2f..c64da98 100644 --- a/app/templates/threads/thread.html +++ b/app/templates/threads/thread.html @@ -72,5 +72,16 @@ + + {% endblock %}