from flask import Blueprint, request, url_for from ..lib.babycode import babycode_to_html from ..constants import REACTION_EMOJI from .users import is_logged_in, get_active_user from ..models import APIRateLimits, Threads, Reactions, Users, BookmarkCollections, BookmarkedThreads, BookmarkedPosts 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} @bp.post('/add-reaction/') def add_reaction(post_id): if not is_logged_in(): return {'error': 'not authorized', 'error_code': 401}, 401 user = get_active_user() reaction_text = request.json.get('emoji') if not reaction_text or not isinstance(reaction_text, str): return {'error': 'emoji field missing or invalid type', 'error_code': 400}, 400 if reaction_text not in REACTION_EMOJI: return {'error': 'unsupported reaction', 'error_code': 400}, 400 reaction = Reactions.find({ 'user_id': user.id, 'post_id': int(post_id), 'reaction_text': reaction_text, }) if reaction: return {'error': 'reaction already exists', 'error_code': 409}, 409 reaction = Reactions.create({ 'user_id': user.id, 'post_id': int(post_id), 'reaction_text': reaction_text, }) return {'status': 'added'} @bp.post('/remove-reaction/') def remove_reaction(post_id): if not is_logged_in(): return {'error': 'not authorized'}, 401 user = get_active_user() reaction_text = request.json.get('emoji') if not reaction_text or not isinstance(reaction_text, str): return {'error': 'emoji field missing or invalid type'}, 400 if reaction_text not in REACTION_EMOJI: return {'error': 'unsupported reaction'}, 400 reaction = Reactions.find({ 'user_id': user.id, 'post_id': int(post_id), 'reaction_text': reaction_text, }) if not reaction: return {'error': 'reaction does not exist'}, 404 reaction.delete() return {'status': 'removed'} @bp.post('/manage-bookmark-collections/') def manage_bookmark_collections(user_id): if not is_logged_in(): return {'error': 'not authorized', 'error_code': 401}, 401 target_user = Users.find({'id': user_id}) if target_user.id != get_active_user().id: return {'error': 'forbidden', 'error_code': 403}, 403 if target_user.is_guest(): return {'error': 'forbidden', 'error_code': 403}, 403 collections_data = request.json for idx, coll_data in enumerate(collections_data.get('collections')): if coll_data['is_new']: collection = BookmarkCollections.create({ 'name': coll_data['name'], 'user_id': target_user.id, 'sort_order': idx, }) else: collection = BookmarkCollections.find({'id': coll_data['id']}) if not collection: continue update = {'name': coll_data['name']} if not collection.is_default: update['sort_order'] = idx collection.update(update) for removed_id in collections_data.get('removed_collections'): collection = BookmarkCollections.find({'id': removed_id}) if not collection: continue if collection.is_default: continue collection.delete() return {'status': 'ok'}, 200 @bp.post('/bookmark-post/') def bookmark_post(post_id): if not is_logged_in(): return {'error': 'not authorized', 'error_code': 401}, 401 operation = request.json.get('operation') if operation == 'remove' and request.json.get('collection_id', '') == '': return {'status': 'not modified'}, 304 collection_id = int(request.json.get('collection_id')) post_id = int(post_id) memo = request.json.get('memo', '') if operation == 'move': bm = BookmarkedPosts.find({'post_id': post_id}) if not bm: BookmarkedPosts.create({ 'post_id': post_id, 'collection_id': collection_id, 'note': memo, }) else: bm.update({ 'collection_id': collection_id, 'note': memo, }) elif operation == 'remove': bm = BookmarkedPosts.find({'post_id': post_id}) if bm: bm.delete() else: return {'error': 'bad request'}, 400 return {'status': 'ok'}, 200 @bp.post('/bookmark-thread/') def bookmark_thread(thread_id): if not is_logged_in(): return {'error': 'not authorized', 'error_code': 401}, 401 operation = request.json.get('operation') if operation == 'remove' and request.json.get('collection_id', '') == '': return {'status': 'not modified'}, 304 collection_id = int(request.json.get('collection_id')) thread_id = int(thread_id) memo = request.json.get('memo', '') if operation == 'move': bm = BookmarkedThreads.find({'thread_id': thread_id}) if not bm: BookmarkedThreads.create({ 'thread_id': thread_id, 'collection_id': collection_id, 'note': memo, }) else: bm.update({ 'collection_id': collection_id, 'note': memo, }) elif operation == 'remove': bm = BookmarkedThreads.find({ 'thread_id': thread_id, 'note': memo, }) if bm: bm.delete() else: return {'error': 'bad request'}, 400 return {'status': 'ok'}, 200