mirror of
https://codeberg.org/rimu/pyfedi
synced 2025-01-23 11:26:56 -08:00
Add inital routes for API (with minimal changes to main codebase)
This commit is contained in:
parent
37d3501136
commit
1e06b42099
16 changed files with 1387 additions and 0 deletions
|
@ -108,6 +108,9 @@ def create_app(config_class=Config):
|
||||||
from app.dev import bp as dev_bp
|
from app.dev import bp as dev_bp
|
||||||
app.register_blueprint(dev_bp)
|
app.register_blueprint(dev_bp)
|
||||||
|
|
||||||
|
from app.api.alpha import bp as app_api_bp
|
||||||
|
app.register_blueprint(app_api_bp)
|
||||||
|
|
||||||
# send error reports via email
|
# send error reports via email
|
||||||
if app.config['MAIL_SERVER'] and app.config['MAIL_ERRORS']:
|
if app.config['MAIL_SERVER'] and app.config['MAIL_ERRORS']:
|
||||||
auth = None
|
auth = None
|
||||||
|
|
5
app/api/alpha/__init__.py
Normal file
5
app/api/alpha/__init__.py
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
from flask import Blueprint
|
||||||
|
|
||||||
|
bp = Blueprint('api_alpha', __name__)
|
||||||
|
|
||||||
|
from app.api.alpha import routes
|
262
app/api/alpha/routes.py
Normal file
262
app/api/alpha/routes.py
Normal file
|
@ -0,0 +1,262 @@
|
||||||
|
from app.api.alpha import bp
|
||||||
|
from app.api.alpha.utils import get_site, \
|
||||||
|
get_post_list, get_post, post_post_like, \
|
||||||
|
get_reply_list, post_reply_like, \
|
||||||
|
get_community_list, get_community, \
|
||||||
|
get_user
|
||||||
|
from app.shared.auth import log_user_in
|
||||||
|
|
||||||
|
from flask import current_app, jsonify, request
|
||||||
|
|
||||||
|
|
||||||
|
# Site
|
||||||
|
@bp.route('/api/alpha/site', methods=['GET'])
|
||||||
|
def get_alpha_site():
|
||||||
|
if not current_app.debug:
|
||||||
|
return jsonify({'error': 'alpha api routes only available in debug mode'})
|
||||||
|
try:
|
||||||
|
auth = request.headers.get('Authorization')
|
||||||
|
return jsonify(get_site(auth))
|
||||||
|
except Exception as ex:
|
||||||
|
return jsonify({"error": str(ex)}), 400
|
||||||
|
|
||||||
|
|
||||||
|
# Community
|
||||||
|
@bp.route('/api/alpha/community', methods=['GET'])
|
||||||
|
def get_alpha_community():
|
||||||
|
if not current_app.debug:
|
||||||
|
return jsonify({'error': 'alpha api routes only available in debug mode'})
|
||||||
|
try:
|
||||||
|
auth = None
|
||||||
|
data = request.args.to_dict() or None
|
||||||
|
return jsonify(get_community(auth, data))
|
||||||
|
except Exception as ex:
|
||||||
|
return jsonify({"error": str(ex)}), 400
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route('/api/alpha/community/list', methods=['GET'])
|
||||||
|
def get_alpha_community_list():
|
||||||
|
if not current_app.debug:
|
||||||
|
return jsonify({'error': 'alpha api routes only available in debug mode'})
|
||||||
|
try:
|
||||||
|
auth = request.headers.get('Authorization')
|
||||||
|
data = request.args.to_dict() or None
|
||||||
|
return jsonify(get_community_list(auth, data))
|
||||||
|
except Exception as ex:
|
||||||
|
return jsonify({"error": str(ex)}), 400
|
||||||
|
|
||||||
|
|
||||||
|
# Post
|
||||||
|
@bp.route('/api/alpha/post/list', methods=['GET'])
|
||||||
|
def get_alpha_post_list():
|
||||||
|
if not current_app.debug:
|
||||||
|
return jsonify({'error': 'alpha api routes only available in debug mode'})
|
||||||
|
try:
|
||||||
|
auth = request.headers.get('Authorization')
|
||||||
|
data = request.args.to_dict() or None
|
||||||
|
return jsonify(get_post_list(auth, data))
|
||||||
|
except Exception as ex:
|
||||||
|
return jsonify({"error": str(ex)}), 400
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route('/api/alpha/post', methods=['GET'])
|
||||||
|
def get_alpha_post():
|
||||||
|
if not current_app.debug:
|
||||||
|
return jsonify({'error': 'alpha api routes only available in debug mode'})
|
||||||
|
try:
|
||||||
|
auth = request.headers.get('Authorization')
|
||||||
|
data = request.args.to_dict() or None
|
||||||
|
return jsonify(get_post(auth, data))
|
||||||
|
except Exception as ex:
|
||||||
|
return jsonify({"error": str(ex)}), 400
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route('/api/alpha/post/like', methods=['POST'])
|
||||||
|
def post_alpha_post_like():
|
||||||
|
if not current_app.debug:
|
||||||
|
return jsonify({'error': 'alpha api routes only available in debug mode'})
|
||||||
|
try:
|
||||||
|
auth = request.headers.get('Authorization')
|
||||||
|
data = request.get_json(force=True) or {}
|
||||||
|
return jsonify(post_post_like(auth, data))
|
||||||
|
except Exception as ex:
|
||||||
|
return jsonify({"error": str(ex)}), 400
|
||||||
|
|
||||||
|
|
||||||
|
# Reply
|
||||||
|
@bp.route('/api/alpha/comment/list', methods=['GET'])
|
||||||
|
def get_alpha_comment_list():
|
||||||
|
if not current_app.debug:
|
||||||
|
return jsonify({'error': 'alpha api routes only available in debug mode'})
|
||||||
|
try:
|
||||||
|
auth = request.headers.get('Authorization')
|
||||||
|
data = request.args.to_dict() or None
|
||||||
|
return jsonify(get_reply_list(auth, data))
|
||||||
|
except Exception as ex:
|
||||||
|
return jsonify({"error": str(ex)}), 400
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route('/api/alpha/comment/like', methods=['POST'])
|
||||||
|
def post_alpha_comment_like():
|
||||||
|
if not current_app.debug:
|
||||||
|
return jsonify({'error': 'alpha api routes only available in debug mode'})
|
||||||
|
try:
|
||||||
|
auth = request.headers.get('Authorization')
|
||||||
|
data = request.get_json(force=True) or {}
|
||||||
|
return jsonify(post_reply_like(auth, data))
|
||||||
|
except Exception as ex:
|
||||||
|
return jsonify({"error": str(ex)}), 400
|
||||||
|
|
||||||
|
|
||||||
|
# User
|
||||||
|
@bp.route('/api/alpha/user', methods=['GET'])
|
||||||
|
def get_alpha_user():
|
||||||
|
if not current_app.debug:
|
||||||
|
return jsonify({'error': 'alpha api routes only available in debug mode'})
|
||||||
|
try:
|
||||||
|
auth = request.headers.get('Authorization')
|
||||||
|
data = request.args.to_dict() or None
|
||||||
|
return jsonify(get_user(auth, data))
|
||||||
|
except Exception as ex:
|
||||||
|
return jsonify({"error": str(ex)}), 400
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route('/api/alpha/user/login', methods=['POST'])
|
||||||
|
def post_alpha_login():
|
||||||
|
if not current_app.debug:
|
||||||
|
return jsonify({'error': 'alpha api routes only available in debug mode'})
|
||||||
|
try:
|
||||||
|
SRC_API = 3 # would be in app.constants
|
||||||
|
data = request.get_json(force=True) or {}
|
||||||
|
return jsonify(log_user_in(data, SRC_API))
|
||||||
|
except Exception as ex:
|
||||||
|
return jsonify({"error": str(ex)}), 400
|
||||||
|
|
||||||
|
|
||||||
|
# Not yet implemented. Copied from lemmy's V3 api, so some aren't needed, and some need changing
|
||||||
|
|
||||||
|
# Site - not yet implemented
|
||||||
|
@bp.route('/api/alpha/site', methods=['POST'])
|
||||||
|
@bp.route('/api/alpha/site', methods=['PUT'])
|
||||||
|
@bp.route('/api/alpha/site/block', methods=['POST'])
|
||||||
|
def alpha_site():
|
||||||
|
return jsonify({"error": "not_yet_implemented"}), 400
|
||||||
|
|
||||||
|
# Miscellaneous - not yet implemented
|
||||||
|
@bp.route('/api/alpha/modlog', methods=['GET'])
|
||||||
|
@bp.route('/api/alpha/search', methods=['GET'])
|
||||||
|
@bp.route('/api/alpha/resolve_object', methods=['GET'])
|
||||||
|
@bp.route('/api/alpha/federated_instances', methods=['GET'])
|
||||||
|
def alpha_miscellaneous():
|
||||||
|
return jsonify({"error": "not_yet_implemented"}), 400
|
||||||
|
|
||||||
|
# Community - not yet implemented
|
||||||
|
@bp.route('/api/alpha/community', methods=['POST'])
|
||||||
|
@bp.route('/api/alpha/community', methods=['PUT'])
|
||||||
|
@bp.route('/api/alpha/community/hide', methods=['PUT'])
|
||||||
|
@bp.route('/api/alpha/community/follow', methods=['POST'])
|
||||||
|
@bp.route('/api/alpha/community/block', methods=['POST'])
|
||||||
|
@bp.route('/api/alpha/community/delete', methods=['POST'])
|
||||||
|
@bp.route('/api/alpha/community/remove', methods=['POST'])
|
||||||
|
@bp.route('/api/alpha/community/transfer', methods=['POST'])
|
||||||
|
@bp.route('/api/alpha/community/ban_user', methods=['POST'])
|
||||||
|
@bp.route('/api/alpha/community/mod', methods=['POST'])
|
||||||
|
def alpha_community():
|
||||||
|
return jsonify({"error": "not_yet_implemented"}), 400
|
||||||
|
|
||||||
|
# Post - not yet implemented
|
||||||
|
@bp.route('/api/alpha/post', methods=['PUT'])
|
||||||
|
@bp.route('/api/alpha/post', methods=['POST'])
|
||||||
|
@bp.route('/api/alpha/post/delete', methods=['POST'])
|
||||||
|
@bp.route('/api/alpha/post/remove', methods=['POST'])
|
||||||
|
@bp.route('/api/alpha/post/lock', methods=['POST'])
|
||||||
|
@bp.route('/api/alpha/post/feature', methods=['POST'])
|
||||||
|
@bp.route('/api/alpha/post/like', methods=['POST'])
|
||||||
|
@bp.route('/api/alpha/post/save', methods=['PUT'])
|
||||||
|
@bp.route('/api/alpha/post/report', methods=['POST'])
|
||||||
|
@bp.route('/api/alpha/post/report/resolve', methods=['PUT'])
|
||||||
|
@bp.route('/api/alpha/post/report/list', methods=['GET'])
|
||||||
|
@bp.route('/api/alpha/post/site_metadata', methods=['GET'])
|
||||||
|
def alpha_post():
|
||||||
|
return jsonify({"error": "not_yet_implemented"}), 400
|
||||||
|
|
||||||
|
# Reply - not yet implemented
|
||||||
|
@bp.route('/api/alpha/comment', methods=['GET'])
|
||||||
|
@bp.route('/api/alpha/comment', methods=['PUT'])
|
||||||
|
@bp.route('/api/alpha/comment', methods=['POST'])
|
||||||
|
@bp.route('/api/alpha/comment/delete', methods=['POST'])
|
||||||
|
@bp.route('/api/alpha/comment/remove', methods=['POST'])
|
||||||
|
@bp.route('/api/alpha/comment/mark_as_read', methods=['POST'])
|
||||||
|
@bp.route('/api/alpha/comment/distinguish', methods=['POST'])
|
||||||
|
@bp.route('/api/alpha/comment/save', methods=['PUT'])
|
||||||
|
@bp.route('/api/alpha/comment/report', methods=['POST'])
|
||||||
|
@bp.route('/api/alpha/comment/report/resolve', methods=['PUT'])
|
||||||
|
@bp.route('/api/alpha/comment/report/list', methods=['GET'])
|
||||||
|
def alpha_reply():
|
||||||
|
return jsonify({"error": "not_yet_implemented"}), 400
|
||||||
|
|
||||||
|
# Chat - not yet implemented
|
||||||
|
@bp.route('/api/alpha/private_message/list', methods=['GET'])
|
||||||
|
@bp.route('/api/alpha/private_message', methods=['PUT'])
|
||||||
|
@bp.route('/api/alpha/private_message', methods=['POST'])
|
||||||
|
@bp.route('/api/alpha/private_message/delete', methods=['POST'])
|
||||||
|
@bp.route('/api/alpha/private_message/mark_as_read', methods=['POST'])
|
||||||
|
@bp.route('/api/alpha/private_message/report', methods=['POST'])
|
||||||
|
@bp.route('/api/alpha/private_message/report/resolve', methods=['PUT'])
|
||||||
|
@bp.route('/api/alpha/private_message/report/list', methods=['GET'])
|
||||||
|
def alpha_chat():
|
||||||
|
return jsonify({"error": "not_yet_implemented"}), 400
|
||||||
|
|
||||||
|
# User - not yet implemented
|
||||||
|
@bp.route('/api/alpha/user/register', methods=['POST'])
|
||||||
|
@bp.route('/api/alpha/user/get_captcha', methods=['GET'])
|
||||||
|
@bp.route('/api/alpha/user/mention', methods=['GET'])
|
||||||
|
@bp.route('/api/alpha/user/mention/mark_as_read', methods=['POST'])
|
||||||
|
@bp.route('/api/alpha/user/replies', methods=['GET'])
|
||||||
|
@bp.route('/api/alpha/user/ban', methods=['POST'])
|
||||||
|
@bp.route('/api/alpha/user/banned', methods=['GET'])
|
||||||
|
@bp.route('/api/alpha/user/block', methods=['POST'])
|
||||||
|
@bp.route('/api/alpha/user/delete_account', methods=['POST'])
|
||||||
|
@bp.route('/api/alpha/user/password_reset', methods=['POST'])
|
||||||
|
@bp.route('/api/alpha/user/password_change', methods=['POST'])
|
||||||
|
@bp.route('/api/alpha/user/mark_all_as_read', methods=['POST'])
|
||||||
|
@bp.route('/api/alpha/user/save_user_settings', methods=['PUT'])
|
||||||
|
@bp.route('/api/alpha/user/change_password', methods=['PUT'])
|
||||||
|
@bp.route('/api/alpha/user/repost_count', methods=['GET'])
|
||||||
|
@bp.route('/api/alpha/user/unread_count', methods=['GET'])
|
||||||
|
@bp.route('/api/alpha/user/verify_email', methods=['POST'])
|
||||||
|
@bp.route('/api/alpha/user/leave_admin', methods=['POST'])
|
||||||
|
@bp.route('/api/alpha/user/totp/generate', methods=['POST'])
|
||||||
|
@bp.route('/api/alpha/user/totp/update', methods=['POST'])
|
||||||
|
@bp.route('/api/alpha/user/export_settings', methods=['GET'])
|
||||||
|
@bp.route('/api/alpha/user/import_settings', methods=['POST'])
|
||||||
|
@bp.route('/api/alpha/user/list_logins', methods=['GET'])
|
||||||
|
@bp.route('/api/alpha/user/validate_auth', methods=['GET'])
|
||||||
|
@bp.route('/api/alpha/user/logout', methods=['POST'])
|
||||||
|
def alpha_user():
|
||||||
|
return jsonify({"error": "not_yet_implemented"}), 400
|
||||||
|
|
||||||
|
# Admin - not yet implemented
|
||||||
|
@bp.route('/api/alpha/admin/add', methods=['POST'])
|
||||||
|
@bp.route('/api/alpha/admin/registration_application/count', methods=['GET'])
|
||||||
|
@bp.route('/api/alpha/admin/registration_application/list', methods=['GET'])
|
||||||
|
@bp.route('/api/alpha/admin/registration_application/approve', methods=['PUT'])
|
||||||
|
@bp.route('/api/alpha/admin/purge/person', methods=['POST'])
|
||||||
|
@bp.route('/api/alpha/admin/purge/community', methods=['POST'])
|
||||||
|
@bp.route('/api/alpha/admin/purge/post', methods=['POST'])
|
||||||
|
@bp.route('/api/alpha/admin/purge/comment', methods=['POST'])
|
||||||
|
@bp.route('/api/alpha/post/like/list', methods=['GET'])
|
||||||
|
@bp.route('/api/alpha/comment/like/list', methods=['GET'])
|
||||||
|
def alpha_admin():
|
||||||
|
return jsonify({"error": "not_yet_implemented"}), 400
|
||||||
|
|
||||||
|
# CustomEmoji - not yet implemented
|
||||||
|
@bp.route('/api/alpha/custom_emoji', methods=['PUT'])
|
||||||
|
@bp.route('/api/alpha/custom_emoji', methods=['POST'])
|
||||||
|
@bp.route('/api/alpha/custom_emoji/delete', methods=['POST'])
|
||||||
|
def alpha_emoji():
|
||||||
|
return jsonify({"error": "not_yet_implemented"}), 400
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
7
app/api/alpha/utils/__init__.py
Normal file
7
app/api/alpha/utils/__init__.py
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
from app.api.alpha.utils.site import get_site
|
||||||
|
from app.api.alpha.utils.post import get_post_list, get_post, post_post_like
|
||||||
|
from app.api.alpha.utils.reply import get_reply_list, post_reply_like
|
||||||
|
from app.api.alpha.utils.community import get_community, get_community_list
|
||||||
|
from app.api.alpha.utils.user import get_user
|
||||||
|
|
||||||
|
|
62
app/api/alpha/utils/community.py
Normal file
62
app/api/alpha/utils/community.py
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
from app import cache
|
||||||
|
from app.api.alpha.views import community_view
|
||||||
|
from app.utils import authorise_api_user
|
||||||
|
from app.models import Community, CommunityMember
|
||||||
|
from app.utils import communities_banned_from
|
||||||
|
|
||||||
|
|
||||||
|
@cache.memoize(timeout=3)
|
||||||
|
def cached_community_list(type, user_id):
|
||||||
|
if type == 'Subscribed' and user_id is not None:
|
||||||
|
communities = Community.query.filter_by(banned=False).join(CommunityMember).filter(CommunityMember.user_id == user_id)
|
||||||
|
banned_from = communities_banned_from(user_id)
|
||||||
|
if banned_from:
|
||||||
|
communities = communities.filter(Community.id.not_in(banned_from))
|
||||||
|
else:
|
||||||
|
communities = Community.query.filter_by(banned=False)
|
||||||
|
|
||||||
|
return communities.all()
|
||||||
|
|
||||||
|
|
||||||
|
def get_community_list(auth, data):
|
||||||
|
type = data['type_'] if data and 'type_' in data else "All"
|
||||||
|
page = int(data['page']) if data and 'page' in data else 1
|
||||||
|
limit = int(data['limit']) if data and 'limit' in data else 10
|
||||||
|
|
||||||
|
if auth:
|
||||||
|
try:
|
||||||
|
user_id = authorise_api_user(auth)
|
||||||
|
except Exception as e:
|
||||||
|
raise e
|
||||||
|
else:
|
||||||
|
user_id = None
|
||||||
|
|
||||||
|
communities = cached_community_list(type, user_id)
|
||||||
|
|
||||||
|
start = (page - 1) * limit
|
||||||
|
end = start + limit
|
||||||
|
communities = communities[start:end]
|
||||||
|
|
||||||
|
communitylist = []
|
||||||
|
for community in communities:
|
||||||
|
communitylist.append(community_view(community=community, variant=2, stub=True, user_id=user_id))
|
||||||
|
list_json = {
|
||||||
|
"communities": communitylist
|
||||||
|
}
|
||||||
|
|
||||||
|
return list_json
|
||||||
|
|
||||||
|
|
||||||
|
def get_community(auth, data):
|
||||||
|
if not data or ('id' not in data and 'name' not in data):
|
||||||
|
raise Exception('missing_parameters')
|
||||||
|
if 'id' in data:
|
||||||
|
community = int(data['id'])
|
||||||
|
elif 'name' in data:
|
||||||
|
community = data['name']
|
||||||
|
|
||||||
|
try:
|
||||||
|
community_json = community_view(community=community, variant=3)
|
||||||
|
return community_json
|
||||||
|
except:
|
||||||
|
raise
|
134
app/api/alpha/utils/post.py
Normal file
134
app/api/alpha/utils/post.py
Normal file
|
@ -0,0 +1,134 @@
|
||||||
|
from app import cache
|
||||||
|
from app.api.alpha.views import post_view
|
||||||
|
from app.api.alpha.utils.validators import required, integer_expected
|
||||||
|
from app.models import Post, Community, CommunityMember, utcnow
|
||||||
|
from app.shared.post import vote_for_post
|
||||||
|
from app.utils import authorise_api_user
|
||||||
|
|
||||||
|
from datetime import timedelta
|
||||||
|
from sqlalchemy import desc
|
||||||
|
|
||||||
|
|
||||||
|
@cache.memoize(timeout=3)
|
||||||
|
def cached_post_list(type, sort, user_id, community_id, community_name, person_id):
|
||||||
|
if type == "All":
|
||||||
|
if community_name:
|
||||||
|
posts = Post.query.filter_by(deleted=False).join(Community, Community.id == Post.community_id).filter_by(show_all=True, name=community_name)
|
||||||
|
elif community_id:
|
||||||
|
posts = Post.query.filter_by(deleted=False).join(Community, Community.id == Post.community_id).filter_by(show_all=True, id=community_id)
|
||||||
|
elif person_id:
|
||||||
|
posts = Post.query.filter_by(deleted=False, user_id=person_id)
|
||||||
|
else:
|
||||||
|
posts = Post.query.filter_by(deleted=False).join(Community, Community.id == Post.community_id).filter_by(show_all=True)
|
||||||
|
elif type == "Local":
|
||||||
|
posts = Post.query.filter_by(deleted=False).join(Community, Community.id == Post.community_id).filter_by(ap_id=None)
|
||||||
|
elif type == "Subscribed" and user_id is not None:
|
||||||
|
posts = Post.query.filter_by(deleted=False).join(CommunityMember, Post.community_id == CommunityMember.community_id).filter_by(is_banned=False, user_id=user_id)
|
||||||
|
else:
|
||||||
|
posts = Post.query.filter_by(deleted=False)
|
||||||
|
|
||||||
|
if sort == "Hot":
|
||||||
|
posts = posts.order_by(desc(Post.ranking)).order_by(desc(Post.posted_at))
|
||||||
|
elif sort == "TopDay":
|
||||||
|
posts = posts.filter(Post.posted_at > utcnow() - timedelta(days=1)).order_by(desc(Post.up_votes - Post.down_votes))
|
||||||
|
elif sort == "New":
|
||||||
|
posts = posts.order_by(desc(Post.posted_at))
|
||||||
|
elif sort == "Active":
|
||||||
|
posts = posts.order_by(desc(Post.last_active))
|
||||||
|
|
||||||
|
return posts.all()
|
||||||
|
|
||||||
|
|
||||||
|
def get_post_list(auth, data, user_id=None):
|
||||||
|
type = data['type_'] if data and 'type_' in data else "All"
|
||||||
|
sort = data['sort'] if data and 'sort' in data else "Hot"
|
||||||
|
page = int(data['page']) if data and 'page' in data else 1
|
||||||
|
limit = int(data['limit']) if data and 'limit' in data else 10
|
||||||
|
|
||||||
|
if auth:
|
||||||
|
try:
|
||||||
|
user_id = authorise_api_user(auth)
|
||||||
|
except Exception as e:
|
||||||
|
raise e
|
||||||
|
|
||||||
|
# user_id: the logged in user
|
||||||
|
# person_id: the author of the posts being requested
|
||||||
|
|
||||||
|
community_id = int(data['community_id']) if data and 'community_id' in data else None
|
||||||
|
community_name = data['community_name'] if data and 'community_name' in data else None
|
||||||
|
person_id = int(data['person_id']) if data and 'person_id' in data else None
|
||||||
|
|
||||||
|
posts = cached_post_list(type, sort, user_id, community_id, community_name, person_id)
|
||||||
|
|
||||||
|
start = (page - 1) * limit
|
||||||
|
end = start + limit
|
||||||
|
posts = posts[start:end]
|
||||||
|
|
||||||
|
postlist = []
|
||||||
|
for post in posts:
|
||||||
|
try:
|
||||||
|
postlist.append(post_view(post=post, variant=2, stub=True, user_id=user_id))
|
||||||
|
except:
|
||||||
|
continue
|
||||||
|
list_json = {
|
||||||
|
"posts": postlist
|
||||||
|
}
|
||||||
|
|
||||||
|
return list_json
|
||||||
|
|
||||||
|
|
||||||
|
def get_post(auth, data):
|
||||||
|
if not data or 'id' not in data:
|
||||||
|
raise Exception('missing_parameters')
|
||||||
|
|
||||||
|
id = int(data['id'])
|
||||||
|
|
||||||
|
if auth:
|
||||||
|
try:
|
||||||
|
user_id = authorise_api_user(auth)
|
||||||
|
except Exception as e:
|
||||||
|
raise e
|
||||||
|
else:
|
||||||
|
user_id = None
|
||||||
|
|
||||||
|
post_json = post_view(post=id, variant=3, user_id=user_id)
|
||||||
|
if post_json:
|
||||||
|
return post_json
|
||||||
|
else:
|
||||||
|
raise Exception('post_not_found')
|
||||||
|
|
||||||
|
|
||||||
|
# would be in app/constants.py
|
||||||
|
SRC_API = 3
|
||||||
|
|
||||||
|
def post_post_like(auth, data):
|
||||||
|
try:
|
||||||
|
required(['post_id', 'score'], data)
|
||||||
|
integer_expected(['post_id', 'score'], data)
|
||||||
|
except:
|
||||||
|
raise
|
||||||
|
|
||||||
|
post_id = data['post_id']
|
||||||
|
score = data['score']
|
||||||
|
if score == 1:
|
||||||
|
direction = 'upvote'
|
||||||
|
elif score == -1:
|
||||||
|
direction = 'downvote'
|
||||||
|
else:
|
||||||
|
score = 0
|
||||||
|
direction = 'reversal'
|
||||||
|
|
||||||
|
try:
|
||||||
|
user_id = vote_for_post(post_id, direction, SRC_API, auth)
|
||||||
|
cache.delete_memoized(cached_post_list)
|
||||||
|
post_json = post_view(post=post_id, variant=4, user_id=user_id, my_vote=score)
|
||||||
|
return post_json
|
||||||
|
except:
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
96
app/api/alpha/utils/reply.py
Normal file
96
app/api/alpha/utils/reply.py
Normal file
|
@ -0,0 +1,96 @@
|
||||||
|
from app import cache
|
||||||
|
from app.utils import authorise_api_user
|
||||||
|
from app.api.alpha.utils.validators import required, integer_expected
|
||||||
|
from app.api.alpha.views import reply_view
|
||||||
|
from app.models import PostReply
|
||||||
|
from app.shared.reply import vote_for_reply
|
||||||
|
|
||||||
|
from sqlalchemy import desc
|
||||||
|
|
||||||
|
|
||||||
|
@cache.memoize(timeout=3)
|
||||||
|
def cached_reply_list(post_id, person_id, sort, max_depth):
|
||||||
|
if post_id:
|
||||||
|
replies = PostReply.query.filter(PostReply.deleted == False, PostReply.post_id == post_id, PostReply.depth <= max_depth)
|
||||||
|
if person_id:
|
||||||
|
replies = PostReply.query.filter_by(deleted=False, user_id=person_id)
|
||||||
|
|
||||||
|
if sort == "Hot":
|
||||||
|
replies = replies.order_by(desc(PostReply.ranking)).order_by(desc(PostReply.posted_at))
|
||||||
|
elif sort == "Top":
|
||||||
|
replies = replies.order_by(desc(PostReply.up_votes - PostReply.down_votes))
|
||||||
|
elif sort == "New":
|
||||||
|
replies = replies.order_by(desc(PostReply.posted_at))
|
||||||
|
|
||||||
|
return replies.all()
|
||||||
|
|
||||||
|
|
||||||
|
def get_reply_list(auth, data, user_id=None):
|
||||||
|
sort = data['sort'] if data and 'sort' in data else "New"
|
||||||
|
max_depth = data['max_depth'] if data and 'max_depth' in data else 8
|
||||||
|
page = int(data['page']) if data and 'page' in data else 1
|
||||||
|
limit = int(data['limit']) if data and 'limit' in data else 10
|
||||||
|
post_id = data['post_id'] if data and 'post_id' in data else None
|
||||||
|
person_id = data['person_id'] if data and 'person_id' in data else None
|
||||||
|
|
||||||
|
if data and not post_id and not person_id:
|
||||||
|
raise Exception('missing_parameters')
|
||||||
|
else:
|
||||||
|
replies = cached_reply_list(post_id, person_id, sort, max_depth)
|
||||||
|
|
||||||
|
if auth:
|
||||||
|
try:
|
||||||
|
user_id = authorise_api_user(auth)
|
||||||
|
except Exception as e:
|
||||||
|
raise e
|
||||||
|
|
||||||
|
# user_id: the logged in user
|
||||||
|
# person_id: the author of the posts being requested
|
||||||
|
|
||||||
|
start = (page - 1) * limit
|
||||||
|
end = start + limit
|
||||||
|
replies = replies[start:end]
|
||||||
|
|
||||||
|
replylist = []
|
||||||
|
for reply in replies:
|
||||||
|
try:
|
||||||
|
replylist.append(reply_view(reply=reply, variant=2, user_id=user_id))
|
||||||
|
except:
|
||||||
|
continue
|
||||||
|
list_json = {
|
||||||
|
"comments": replylist
|
||||||
|
}
|
||||||
|
|
||||||
|
return list_json
|
||||||
|
|
||||||
|
|
||||||
|
# would be in app/constants.py
|
||||||
|
SRC_API = 3
|
||||||
|
|
||||||
|
def post_reply_like(auth, data):
|
||||||
|
try:
|
||||||
|
required(['comment_id', 'score'], data)
|
||||||
|
integer_expected(['comment_id', 'score'], data)
|
||||||
|
except:
|
||||||
|
raise
|
||||||
|
|
||||||
|
score = data['score']
|
||||||
|
reply_id = data['comment_id']
|
||||||
|
if score == 1:
|
||||||
|
direction = 'upvote'
|
||||||
|
elif score == -1:
|
||||||
|
direction = 'downvote'
|
||||||
|
else:
|
||||||
|
score = 0
|
||||||
|
direction = 'reversal'
|
||||||
|
|
||||||
|
try:
|
||||||
|
user_id = vote_for_reply(reply_id, direction, SRC_API, auth)
|
||||||
|
cache.delete_memoized(cached_reply_list)
|
||||||
|
reply_json = reply_view(reply=reply_id, variant=4, user_id=user_id, my_vote=score)
|
||||||
|
return reply_json
|
||||||
|
except:
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
|
95
app/api/alpha/utils/site.py
Normal file
95
app/api/alpha/utils/site.py
Normal file
|
@ -0,0 +1,95 @@
|
||||||
|
from app import db
|
||||||
|
from app.utils import authorise_api_user
|
||||||
|
from app.models import Language
|
||||||
|
|
||||||
|
from flask import current_app, g
|
||||||
|
|
||||||
|
from sqlalchemy import text
|
||||||
|
|
||||||
|
def users_total():
|
||||||
|
return db.session.execute(text(
|
||||||
|
'SELECT COUNT(id) as c FROM "user" WHERE ap_id is null AND verified is true AND banned is false AND deleted is false')).scalar()
|
||||||
|
|
||||||
|
def get_site(auth):
|
||||||
|
if auth:
|
||||||
|
try:
|
||||||
|
user = authorise_api_user(auth, return_type='model')
|
||||||
|
except Exception as e:
|
||||||
|
raise e
|
||||||
|
else:
|
||||||
|
user = None
|
||||||
|
|
||||||
|
logo = g.site.logo if g.site.logo else '/static/images/logo2.png'
|
||||||
|
site = {
|
||||||
|
"enable_downvotes": g.site.enable_downvotes,
|
||||||
|
"icon": f"https://{current_app.config['SERVER_NAME']}{logo}",
|
||||||
|
"name": g.site.name,
|
||||||
|
"actor_id": f"https://{current_app.config['SERVER_NAME']}/",
|
||||||
|
"user_count": users_total(),
|
||||||
|
"all_languages": []
|
||||||
|
}
|
||||||
|
if g.site.sidebar:
|
||||||
|
site['sidebar'] = g.site.sidebar
|
||||||
|
if g.site.description:
|
||||||
|
site['description'] = g.site.description
|
||||||
|
for language in Language.query.all():
|
||||||
|
site["all_languages"].append({
|
||||||
|
"id": language.id,
|
||||||
|
"code": language.code,
|
||||||
|
"name": language.name
|
||||||
|
})
|
||||||
|
|
||||||
|
if user:
|
||||||
|
my_user = {
|
||||||
|
"local_user_view": {
|
||||||
|
"local_user": {
|
||||||
|
"show_nsfw": not user.hide_nsfw == 1,
|
||||||
|
"default_sort_type": user.default_sort.capitalize(),
|
||||||
|
"default_listing_type": user.default_filter.capitalize(),
|
||||||
|
"show_scores": True,
|
||||||
|
"show_bot_accounts": not user.ignore_bots == 1,
|
||||||
|
"show_read_posts": True,
|
||||||
|
},
|
||||||
|
"person": {
|
||||||
|
"id": user.id,
|
||||||
|
"user_name": user.user_name,
|
||||||
|
"banned": user.banned,
|
||||||
|
"published": user.created.isoformat() + 'Z',
|
||||||
|
"actor_id": user.public_url()[8:],
|
||||||
|
"local": True,
|
||||||
|
"deleted": user.deleted,
|
||||||
|
"bot": user.bot,
|
||||||
|
"instance_id": 1
|
||||||
|
},
|
||||||
|
"counts": {
|
||||||
|
"person_id": user.id,
|
||||||
|
"post_count": user.post_count,
|
||||||
|
"comment_count": user.post_reply_count
|
||||||
|
}
|
||||||
|
},
|
||||||
|
#"moderates": [],
|
||||||
|
#"follows": [],
|
||||||
|
"community_blocks": [], # TODO
|
||||||
|
"instance_blocks": [], # TODO
|
||||||
|
"person_blocks": [], # TODO
|
||||||
|
"discussion_languages": [] # TODO
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
Note: Thunder doesn't use moderates[] and follows[] from here, but it would be more efficient if it did (rather than getting them from /user and /community)
|
||||||
|
cms = CommunityMember.query.filter_by(user_id=user_id, is_moderator=True)
|
||||||
|
for cm in cms:
|
||||||
|
my_user['moderates'].append({'community': Community.api_json(variant=1, id=cm.community_id, stub=True), 'moderator': User.api_json(variant=1, id=user_id, stub=True)})
|
||||||
|
cms = CommunityMember.query.filter_by(user_id=user_id, is_banned=False)
|
||||||
|
for cm in cms:
|
||||||
|
my_user['follows'].append({'community': Community.api_json(variant=1, id=cm.community_id, stub=True), 'follower': User.api_json(variant=1, id=user_id, stub=True)})
|
||||||
|
"""
|
||||||
|
|
||||||
|
data = {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"site": site
|
||||||
|
}
|
||||||
|
if user:
|
||||||
|
data['my_user'] = my_user
|
||||||
|
|
||||||
|
return data
|
||||||
|
|
36
app/api/alpha/utils/user.py
Normal file
36
app/api/alpha/utils/user.py
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
from app.api.alpha.views import user_view
|
||||||
|
from app.utils import authorise_api_user
|
||||||
|
from app.api.alpha.utils.post import get_post_list
|
||||||
|
from app.api.alpha.utils.reply import get_reply_list
|
||||||
|
|
||||||
|
|
||||||
|
def get_user(auth, data):
|
||||||
|
if not data or ('person_id' not in data and 'username' not in data):
|
||||||
|
raise Exception('missing_parameters')
|
||||||
|
|
||||||
|
person_id = int(data['person_id']) # TODO: handle 'username' (was passed on login, as a way to get subscription list, but temporarily removed)
|
||||||
|
|
||||||
|
user_id = None
|
||||||
|
if auth:
|
||||||
|
try:
|
||||||
|
user_id = authorise_api_user(auth)
|
||||||
|
auth = None
|
||||||
|
except Exception as e:
|
||||||
|
raise e
|
||||||
|
|
||||||
|
# user_id = logged in user, person_id = person who's posts, comments etc are being fetched
|
||||||
|
|
||||||
|
# bit unusual. have to help construct the json here rather than in views, to avoid circular dependencies
|
||||||
|
post_list = get_post_list(auth, data, user_id)
|
||||||
|
reply_list = get_reply_list(auth, data, user_id)
|
||||||
|
|
||||||
|
try:
|
||||||
|
user_json = user_view(user=person_id, variant=3)
|
||||||
|
user_json['posts'] = post_list['posts']
|
||||||
|
user_json['comments'] = reply_list['comments']
|
||||||
|
return user_json
|
||||||
|
except:
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
|
45
app/api/alpha/utils/validators.py
Normal file
45
app/api/alpha/utils/validators.py
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
def string_expected(values: list, data: dict):
|
||||||
|
for v in values:
|
||||||
|
if v in data:
|
||||||
|
if (not isinstance(data[v], str) and not isinstance(data[v], type(None))):
|
||||||
|
raise Exception('string_expected_for_' + v)
|
||||||
|
|
||||||
|
|
||||||
|
def integer_expected(values: list, data: dict):
|
||||||
|
for v in values:
|
||||||
|
if v in data:
|
||||||
|
if (not isinstance(data[v], int) and not isinstance(data[v], type(None))) or isinstance(data[v], bool):
|
||||||
|
raise Exception('integer_expected_for_' + v)
|
||||||
|
|
||||||
|
|
||||||
|
def boolean_expected(values: list, data: dict):
|
||||||
|
for v in values:
|
||||||
|
if v in data:
|
||||||
|
if (not isinstance(data[v], bool) and not isinstance(data[v], type(None))):
|
||||||
|
raise Exception('boolean_expected_for_' + v)
|
||||||
|
|
||||||
|
|
||||||
|
def array_of_strings_expected(values: list, data: dict):
|
||||||
|
for v in values:
|
||||||
|
if v in data:
|
||||||
|
if (not isinstance(data[v], list) and not isinstance(data[v], type(None))):
|
||||||
|
raise Exception('array_expected_for_' + v)
|
||||||
|
for i in data[v]:
|
||||||
|
if not isinstance(i, str):
|
||||||
|
raise Exception('array_of_strings_expected_for_' + v)
|
||||||
|
|
||||||
|
|
||||||
|
def array_of_integers_expected(values: list, data: dict):
|
||||||
|
for v in values:
|
||||||
|
if v in data:
|
||||||
|
if (not isinstance(data[v], list) and not isinstance(data[v], type(None))):
|
||||||
|
raise Exception('array_expected_for_' + v)
|
||||||
|
for i in data[v]:
|
||||||
|
if not isinstance(i, int):
|
||||||
|
raise Exception('array_of_integers_expected_for_' + v)
|
||||||
|
|
||||||
|
|
||||||
|
def required(values: list, data: dict):
|
||||||
|
for v in values:
|
||||||
|
if v not in data:
|
||||||
|
raise Exception('missing_required_' + v + '_field')
|
312
app/api/alpha/views.py
Normal file
312
app/api/alpha/views.py
Normal file
|
@ -0,0 +1,312 @@
|
||||||
|
from app import cache, db
|
||||||
|
from app.constants import *
|
||||||
|
from app.models import Community, CommunityMember, Post, PostReply, PostVote, User
|
||||||
|
|
||||||
|
from sqlalchemy import text
|
||||||
|
|
||||||
|
# 'stub' param: set to True to exclude optional fields
|
||||||
|
|
||||||
|
|
||||||
|
def post_view(post: Post | int, variant, stub=False, user_id=None, my_vote=0):
|
||||||
|
if isinstance(post, int):
|
||||||
|
post = Post.query.get(post)
|
||||||
|
if not post or post.deleted:
|
||||||
|
raise Exception('post_not_found')
|
||||||
|
|
||||||
|
# Variant 1 - models/post/post.dart
|
||||||
|
if variant == 1:
|
||||||
|
include = ['id', 'title', 'user_id', 'community_id', 'deleted', 'nsfw', 'sticky']
|
||||||
|
v1 = {column.name: getattr(post, column.name) for column in post.__table__.columns if column.name in include}
|
||||||
|
v1.update({'published': post.posted_at.isoformat() + 'Z',
|
||||||
|
'ap_id': post.profile_id(),
|
||||||
|
'local': post.is_local(),
|
||||||
|
'language_id': post.language_id if post.language_id else 0,
|
||||||
|
'removed': post.deleted,
|
||||||
|
'locked': not post.comments_enabled})
|
||||||
|
if post.body and not stub:
|
||||||
|
v1['body'] = post.body
|
||||||
|
if post.edited_at:
|
||||||
|
v1['edited_at'] = post.edited_at.isoformat() + 'Z'
|
||||||
|
if post.type == POST_TYPE_LINK or post.type == POST_TYPE_VIDEO:
|
||||||
|
if post.url:
|
||||||
|
v1['url'] = post.url
|
||||||
|
if post.image_id:
|
||||||
|
v1['thumbnail_url'] = post.image.thumbnail_url()
|
||||||
|
if post.image.alt_text:
|
||||||
|
v1['alt_text'] = post.image.alt_text
|
||||||
|
if post.type == POST_TYPE_IMAGE:
|
||||||
|
if post.image_id:
|
||||||
|
v1['url'] = post.image.view_url()
|
||||||
|
v1['thumbnail_url'] = post.image.medium_url()
|
||||||
|
if post.image.alt_text:
|
||||||
|
v1['alt_text'] = post.image.alt_text
|
||||||
|
|
||||||
|
return v1
|
||||||
|
|
||||||
|
# Variant 2 - views/post_view.dart - /post/list api endpoint
|
||||||
|
if variant == 2:
|
||||||
|
# counts - models/post/post_aggregates.dart
|
||||||
|
counts = {'post_id': post.id, 'comments': post.reply_count, 'score': post.score, 'upvotes': post.up_votes, 'downvotes': post.down_votes,
|
||||||
|
'published': post.posted_at.isoformat() + 'Z', 'newest_comment_time': post.last_active.isoformat() + 'Z'}
|
||||||
|
v2 = {'post': post_view(post=post, variant=1, stub=stub), 'counts': counts, 'banned_from_community': False, 'subscribed': 'NotSubscribed',
|
||||||
|
'saved': False, 'read': False, 'hidden': False, 'creator_blocked': False, 'unread_comments': post.reply_count, 'my_vote': my_vote}
|
||||||
|
|
||||||
|
try:
|
||||||
|
creator = user_view(user=post.user_id, variant=1, stub=True)
|
||||||
|
community = community_view(community=post.community_id, variant=1, stub=True)
|
||||||
|
v2.update({'creator': creator, 'community': community})
|
||||||
|
except:
|
||||||
|
raise
|
||||||
|
|
||||||
|
if not stub:
|
||||||
|
banned = db.session.execute(text('SELECT user_id FROM "community_ban" WHERE user_id = :user_id and community_id = :community_id'), {'user_id': post.user_id, 'community_id': post.community_id}).scalar()
|
||||||
|
creator_banned_from_community = True if banned else False
|
||||||
|
moderator = db.session.execute(text('SELECT is_moderator FROM "community_member" WHERE user_id = :user_id and community_id = :community_id'), {'user_id': post.user_id, 'community_id': post.community_id}).scalar()
|
||||||
|
creator_is_moderator = True if moderator else False
|
||||||
|
admin = db.session.execute(text('SELECT user_id FROM "user_role" WHERE user_id = :user_id and role_id = 4'), {'user_id': post.user_id}).scalar()
|
||||||
|
creator_is_admin = True if admin else False
|
||||||
|
v2.update({'creator_banned_from_community': creator_banned_from_community,
|
||||||
|
'creator_is_moderator': creator_is_moderator,
|
||||||
|
'creator_is_admin': creator_is_admin})
|
||||||
|
|
||||||
|
if my_vote == 0 and user_id is not None:
|
||||||
|
effect = db.session.execute(text('SELECT effect FROM "post_vote" WHERE post_id = :post_id and user_id = :user_id'), {'post_id': post.id, 'user_id': user_id}).scalar()
|
||||||
|
if effect:
|
||||||
|
v2['my_vote'] = int(effect)
|
||||||
|
|
||||||
|
return v2
|
||||||
|
|
||||||
|
# Variant 3 - models/post/get_post_response.dart - /post api endpoint
|
||||||
|
if variant == 3:
|
||||||
|
modlist = cached_modlist_for_community(post.community_id)
|
||||||
|
|
||||||
|
xplist = []
|
||||||
|
if post.cross_posts:
|
||||||
|
for xp_id in post.cross_posts:
|
||||||
|
entry = post_view(post=xp_id, variant=2, stub=True)
|
||||||
|
xplist.append(entry)
|
||||||
|
|
||||||
|
v3 = {'post_view': post_view(post=post, variant=2, user_id=user_id),
|
||||||
|
'community_view': community_view(community=post.community_id, variant=2),
|
||||||
|
'moderators': modlist,
|
||||||
|
'cross_posts': xplist}
|
||||||
|
|
||||||
|
return v3
|
||||||
|
|
||||||
|
# Variant 4 - models/post/post_response.dart - /post/like api endpoint
|
||||||
|
if variant == 4:
|
||||||
|
v4 = {'post_view': post_view(post=post, variant=2, user_id=user_id)}
|
||||||
|
|
||||||
|
return v4
|
||||||
|
|
||||||
|
|
||||||
|
@cache.memoize(timeout=600)
|
||||||
|
def cached_user_view_variant_1(user: User, stub=False):
|
||||||
|
include = ['id', 'user_name', 'title', 'banned', 'deleted', 'bot']
|
||||||
|
v1 = {column.name: getattr(user, column.name) for column in user.__table__.columns if column.name in include}
|
||||||
|
v1.update({'published': user.created.isoformat() + 'Z',
|
||||||
|
'actor_id': user.public_url(),
|
||||||
|
'local': user.is_local(),
|
||||||
|
'instance_id': user.instance_id if user.instance_id else 1})
|
||||||
|
if user.about and not stub:
|
||||||
|
v1['about'] = user.about
|
||||||
|
if user.avatar_id:
|
||||||
|
v1['avatar'] = user.avatar.view_url()
|
||||||
|
if user.cover_id and not stub:
|
||||||
|
v1['banner'] = user.cover.view_url()
|
||||||
|
|
||||||
|
return v1
|
||||||
|
|
||||||
|
|
||||||
|
def user_view(user: User | int, variant, stub=False):
|
||||||
|
if isinstance(user, int):
|
||||||
|
user = User.query.get(user)
|
||||||
|
if not user:
|
||||||
|
raise Exception('user_not_found 1')
|
||||||
|
|
||||||
|
# Variant 1 - models/person/person.dart
|
||||||
|
if variant == 1:
|
||||||
|
return cached_user_view_variant_1(user=user, stub=stub)
|
||||||
|
|
||||||
|
# Variant 2 - views/person_view.dart
|
||||||
|
if variant == 2:
|
||||||
|
counts = {'person_id': user.id, 'post_count': user.post_count, 'comment_count': user.post_reply_count}
|
||||||
|
v2 = {'person': user_view(user=user, variant=1), 'counts': counts, 'is_admin': user.is_admin()}
|
||||||
|
return v2
|
||||||
|
|
||||||
|
# Variant 3 - models/user/get_person_details.dart - /user?person_id api endpoint
|
||||||
|
modlist = cached_modlist_for_user(user)
|
||||||
|
|
||||||
|
v3 = {'person_view': user_view(user=user, variant=2),
|
||||||
|
'moderates': modlist,
|
||||||
|
'posts': [],
|
||||||
|
'comments': []}
|
||||||
|
return v3
|
||||||
|
|
||||||
|
|
||||||
|
@cache.memoize(timeout=600)
|
||||||
|
def cached_community_view_variant_1(community: Community, stub=False):
|
||||||
|
include = ['id', 'name', 'title', 'banned', 'nsfw', 'restricted_to_mods']
|
||||||
|
v1 = {column.name: getattr(community, column.name) for column in community.__table__.columns if column.name in include}
|
||||||
|
v1.update({'published': community.created_at.isoformat() + 'Z',
|
||||||
|
'updated': community.created_at.isoformat() + 'Z',
|
||||||
|
'deleted': False,
|
||||||
|
'removed': False,
|
||||||
|
'actor_id': community.public_url(),
|
||||||
|
'local': community.is_local(),
|
||||||
|
'hidden': not community.show_all,
|
||||||
|
'instance_id': community.instance_id if community.instance_id else 1})
|
||||||
|
if community.description and not stub:
|
||||||
|
v1['description'] = community.description
|
||||||
|
if community.icon_id:
|
||||||
|
v1['icon'] = community.icon.view_url()
|
||||||
|
if community.image_id and not stub:
|
||||||
|
v1['banner'] = community.image.view_url()
|
||||||
|
|
||||||
|
return v1
|
||||||
|
|
||||||
|
|
||||||
|
def community_view(community: Community | int | str, variant, stub=False, user_id=None):
|
||||||
|
if isinstance(community, int):
|
||||||
|
community = Community.query.get(community)
|
||||||
|
elif isinstance(community, str):
|
||||||
|
community = Community.query.filter_by(name=community).first()
|
||||||
|
if not community:
|
||||||
|
raise Exception('community_not_found')
|
||||||
|
|
||||||
|
# Variant 1 - models/community/community.dart
|
||||||
|
if variant == 1:
|
||||||
|
return cached_community_view_variant_1(community=community, stub=stub)
|
||||||
|
|
||||||
|
# Variant 2 - views/community_view.dart - /community/list api endpoint
|
||||||
|
if variant == 2:
|
||||||
|
# counts - models/community/community_aggregates
|
||||||
|
include = ['id', 'subscriptions_count', 'post_count', 'post_reply_count']
|
||||||
|
counts = {column.name: getattr(community, column.name) for column in community.__table__.columns if column.name in include}
|
||||||
|
counts.update({'published': community.created_at.isoformat() + 'Z'})
|
||||||
|
v2 = {'community': community_view(community=community, variant=1, stub=stub), 'subscribed': 'NotSubscribed', 'blocked': False, 'counts': counts}
|
||||||
|
return v2
|
||||||
|
|
||||||
|
# Variant 3 - models/community/get_community_response.dart - /community api endpoint
|
||||||
|
if variant == 3:
|
||||||
|
modlist = cached_modlist_for_community(community.id)
|
||||||
|
|
||||||
|
v3 = {'community_view': community_view(community=community, variant=2),
|
||||||
|
'moderators': modlist,
|
||||||
|
'discussion_languages': []}
|
||||||
|
return v3
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# would be better to incrementally add to a post_reply.path field
|
||||||
|
@cache.memoize(timeout=86400)
|
||||||
|
def calculate_path(reply):
|
||||||
|
path = "0." + str(reply.id)
|
||||||
|
if reply.depth == 1:
|
||||||
|
path = "0." + str(reply.parent_id) + "." + str(reply.id)
|
||||||
|
elif reply.depth > 1:
|
||||||
|
path = "0"
|
||||||
|
parent_id = reply.parent_id
|
||||||
|
depth = reply.depth - 1
|
||||||
|
path_ids = [reply.id, reply.parent_id]
|
||||||
|
while depth > 0:
|
||||||
|
pid = db.session.execute(text('SELECT parent_id FROM "post_reply" WHERE id = :parent_id'), {'parent_id': parent_id}).scalar()
|
||||||
|
path_ids.append(pid)
|
||||||
|
parent_id = pid
|
||||||
|
depth -= 1
|
||||||
|
for pid in path_ids[::-1]:
|
||||||
|
path += "." + str(pid)
|
||||||
|
return path
|
||||||
|
|
||||||
|
|
||||||
|
# would be better to incrementally add to a post_reply.child_count field (walk along .path, and ++ each one)
|
||||||
|
@cache.memoize(timeout=86400)
|
||||||
|
def calculate_if_has_children(reply): # result used as True / False
|
||||||
|
return db.session.execute(text('SELECT COUNT(id) AS c FROM "post_reply" WHERE parent_id = :id'), {'id': reply.id}).scalar()
|
||||||
|
|
||||||
|
|
||||||
|
def reply_view(reply: PostReply | int, variant, user_id=None, my_vote=0):
|
||||||
|
if isinstance(reply, int):
|
||||||
|
reply = PostReply.query.get(reply)
|
||||||
|
if not reply or reply.deleted:
|
||||||
|
raise Exception('reply_not_found')
|
||||||
|
|
||||||
|
|
||||||
|
# Variant 1 - models/comment/comment.dart
|
||||||
|
if variant == 1:
|
||||||
|
include = ['id', 'user_id', 'post_id', 'body', 'deleted']
|
||||||
|
v1 = {column.name: getattr(reply, column.name) for column in reply.__table__.columns if column.name in include}
|
||||||
|
|
||||||
|
v1['path'] = calculate_path(reply)
|
||||||
|
|
||||||
|
v1.update({'published': reply.posted_at.isoformat() + 'Z',
|
||||||
|
'ap_id': reply.profile_id(),
|
||||||
|
'local': reply.is_local(),
|
||||||
|
'language_id': reply.language_id if reply.language_id else 0,
|
||||||
|
'removed': reply.deleted,
|
||||||
|
'distinguished': False})
|
||||||
|
return v1
|
||||||
|
|
||||||
|
# Variant 2 - views/comment_view.dart - /comment/list api endpoint
|
||||||
|
if variant == 2:
|
||||||
|
# counts - models/comment/comment_aggregates.dart
|
||||||
|
counts = {'comment_id': reply.id, 'score': reply.score, 'upvotes': reply.up_votes, 'downvotes': reply.down_votes,
|
||||||
|
'published': reply.posted_at.isoformat() + 'Z', 'child_count': 1 if calculate_if_has_children(reply) else 0}
|
||||||
|
v2 = {'comment': reply_view(reply=reply, variant=1), 'counts': counts, 'banned_from_community': False, 'subscribed': 'NotSubscribed',
|
||||||
|
'saved': False, 'creator_blocked': False, 'my_vote': my_vote}
|
||||||
|
try:
|
||||||
|
creator = user_view(user=reply.user_id, variant=1, stub=True)
|
||||||
|
community = community_view(community=reply.community_id, variant=1, stub=True)
|
||||||
|
post = post_view(post=reply.post_id, variant=1)
|
||||||
|
v2.update({'creator': creator, 'community': community, 'post': post})
|
||||||
|
except:
|
||||||
|
raise
|
||||||
|
|
||||||
|
banned = db.session.execute(text('SELECT user_id FROM "community_ban" WHERE user_id = :user_id and community_id = :community_id'), {'user_id': reply.user_id, 'community_id': reply.community_id}).scalar()
|
||||||
|
creator_banned_from_community = True if banned else False
|
||||||
|
moderator = db.session.execute(text('SELECT is_moderator FROM "community_member" WHERE user_id = :user_id and community_id = :community_id'), {'user_id': reply.user_id, 'community_id': reply.community_id}).scalar()
|
||||||
|
creator_is_moderator = True if moderator else False
|
||||||
|
admin = db.session.execute(text('SELECT user_id FROM "user_role" WHERE user_id = :user_id and role_id = 4'), {'user_id': reply.user_id}).scalar()
|
||||||
|
creator_is_admin = True if admin else False
|
||||||
|
v2.update({'creator_banned_from_community': creator_banned_from_community, 'creator_is_moderator': creator_is_moderator, 'creator_is_admin': creator_is_admin})
|
||||||
|
|
||||||
|
if my_vote == 0 and user_id is not None:
|
||||||
|
effect = db.session.execute(text('SELECT effect FROM "post_reply_vote" WHERE post_reply_id = :post_reply_id and user_id = :user_id'), {'post_reply_id': reply.id, 'user_id': user_id}).scalar()
|
||||||
|
if effect:
|
||||||
|
v2['my_vote'] = int(effect)
|
||||||
|
|
||||||
|
return v2
|
||||||
|
|
||||||
|
# Variant 3 - would be for /comment api endpoint
|
||||||
|
|
||||||
|
# Variant 4 - models/comment/comment_response.dart - /comment/like api endpoint
|
||||||
|
if variant == 4:
|
||||||
|
v4 = {'comment_view': reply_view(reply=reply, variant=2, user_id=user_id)}
|
||||||
|
|
||||||
|
return v4
|
||||||
|
|
||||||
|
|
||||||
|
@cache.memoize(timeout=86400)
|
||||||
|
def cached_modlist_for_community(community_id):
|
||||||
|
moderator_ids = db.session.execute(text('SELECT user_id FROM "community_member" WHERE community_id = :community_id and is_moderator = True'), {'community_id': community_id}).scalars()
|
||||||
|
modlist = []
|
||||||
|
for m_id in moderator_ids:
|
||||||
|
entry = {
|
||||||
|
'community': community_view(community=community_id, variant=1, stub=True),
|
||||||
|
'moderator': user_view(user=m_id, variant=1, stub=True)
|
||||||
|
}
|
||||||
|
modlist.append(entry)
|
||||||
|
return modlist
|
||||||
|
|
||||||
|
|
||||||
|
@cache.memoize(timeout=86400)
|
||||||
|
def cached_modlist_for_user(user):
|
||||||
|
community_ids = db.session.execute(text('SELECT community_id FROM "community_member" WHERE user_id = :user_id and is_moderator = True'), {'user_id': user.id}).scalars()
|
||||||
|
modlist = []
|
||||||
|
for c_id in community_ids:
|
||||||
|
entry = {
|
||||||
|
'community': community_view(community=c_id, variant=1, stub=True),
|
||||||
|
'moderator': user_view(user=user, variant=1, stub=True)
|
||||||
|
}
|
||||||
|
modlist.append(entry)
|
||||||
|
return modlist
|
|
@ -1008,6 +1008,13 @@ class User(UserMixin, db.Model):
|
||||||
return list(db.session.execute(text('SELECT user_id FROM "notification_subscription" WHERE entity_id = :user_id AND type = :type '),
|
return list(db.session.execute(text('SELECT user_id FROM "notification_subscription" WHERE entity_id = :user_id AND type = :type '),
|
||||||
{'user_id': self.id, 'type': NOTIF_USER}).scalars())
|
{'user_id': self.id, 'type': NOTIF_USER}).scalars())
|
||||||
|
|
||||||
|
def encode_jwt_token(self):
|
||||||
|
try:
|
||||||
|
payload = {'sub': str(self.id), 'iss': current_app.config['SERVER_NAME'], 'iat': int(time())}
|
||||||
|
return jwt.encode(payload, current_app.config['SECRET_KEY'], algorithm='HS256')
|
||||||
|
except Exception as e:
|
||||||
|
return str(e)
|
||||||
|
|
||||||
|
|
||||||
class ActivityLog(db.Model):
|
class ActivityLog(db.Model):
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
|
111
app/shared/auth.py
Normal file
111
app/shared/auth.py
Normal file
|
@ -0,0 +1,111 @@
|
||||||
|
from app import db
|
||||||
|
from app.auth.util import ip2location
|
||||||
|
from app.models import IpBan, User, utcnow
|
||||||
|
from app.utils import ip_address, user_ip_banned, user_cookie_banned, banned_ip_addresses, gibberish
|
||||||
|
from app.api.alpha.utils.validators import required, string_expected
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from flask import redirect, url_for, flash, request, make_response, session, Markup
|
||||||
|
from flask_babel import _
|
||||||
|
|
||||||
|
# would be in app/constants.py
|
||||||
|
SRC_WEB = 1
|
||||||
|
SRC_PUB = 2
|
||||||
|
SRC_API = 3
|
||||||
|
|
||||||
|
|
||||||
|
# function can be shared between WEB and API (only API calls it for now)
|
||||||
|
|
||||||
|
def log_user_in(input, src):
|
||||||
|
if src == SRC_WEB:
|
||||||
|
username = input.user_name.data
|
||||||
|
password = input.password.data
|
||||||
|
elif src == SRC_API:
|
||||||
|
try:
|
||||||
|
required(["username_or_email", "password"], input)
|
||||||
|
string_expected(["username_or_email", "password"], input)
|
||||||
|
except Exception:
|
||||||
|
raise
|
||||||
|
|
||||||
|
username = input['username_or_email']
|
||||||
|
password = input['password']
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
user = User.query.filter_by(user_name=username, ap_id=None).first()
|
||||||
|
|
||||||
|
if user is None or user.deleted:
|
||||||
|
if src == SRC_WEB:
|
||||||
|
flash(_('No account exists with that user name.'), 'error')
|
||||||
|
return redirect(url_for('auth.login'))
|
||||||
|
elif src == SRC_API:
|
||||||
|
raise Exception('incorrect_login')
|
||||||
|
|
||||||
|
if not user.check_password(password):
|
||||||
|
if src == SRC_WEB:
|
||||||
|
if user.password_hash is None:
|
||||||
|
message = Markup(_('Invalid password. Please <a href="/auth/reset_password_request">reset your password</a>.'))
|
||||||
|
flash(message, 'error')
|
||||||
|
return redirect(url_for('auth.login'))
|
||||||
|
flash(_('Invalid password'))
|
||||||
|
return redirect(url_for('auth.login'))
|
||||||
|
elif src == SRC_API:
|
||||||
|
raise Exception('incorrect_login')
|
||||||
|
|
||||||
|
if user.id != 1 and (user.banned or user_ip_banned() or user_cookie_banned()):
|
||||||
|
# Detect if a banned user tried to log in from a new IP address
|
||||||
|
if user.banned and not user_ip_banned():
|
||||||
|
# If so, ban their new IP address as well
|
||||||
|
new_ip_ban = IpBan(ip_address=ip_address(), notes=user.user_name + ' used new IP address')
|
||||||
|
db.session.add(new_ip_ban)
|
||||||
|
db.session.commit()
|
||||||
|
cache.delete_memoized(banned_ip_addresses)
|
||||||
|
|
||||||
|
if src == SRC_WEB:
|
||||||
|
flash(_('You have been banned.'), 'error')
|
||||||
|
|
||||||
|
response = make_response(redirect(url_for('auth.login')))
|
||||||
|
|
||||||
|
# Set a cookie so we have another way to track banned people
|
||||||
|
response.set_cookie('sesion', '17489047567495', expires=datetime(year=2099, month=12, day=30))
|
||||||
|
return response
|
||||||
|
elif src == SRC_API:
|
||||||
|
raise Exception('incorrect_login')
|
||||||
|
|
||||||
|
if src == SRC_WEB:
|
||||||
|
if user.waiting_for_approval():
|
||||||
|
return redirect(url_for('auth.please_wait'))
|
||||||
|
login_user(user, remember=True)
|
||||||
|
session['ui_language'] = user.interface_language
|
||||||
|
|
||||||
|
user.last_seen = utcnow()
|
||||||
|
user.ip_address = ip_address()
|
||||||
|
ip_address_info = ip2location(user.ip_address)
|
||||||
|
user.ip_address_country = ip_address_info['country'] if ip_address_info else user.ip_address_country
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
if src == SRC_WEB:
|
||||||
|
next_page = request.args.get('next')
|
||||||
|
if not next_page or url_parse(next_page).netloc != '':
|
||||||
|
if len(user.communities()) == 0:
|
||||||
|
next_page = url_for('topic.choose_topics')
|
||||||
|
else:
|
||||||
|
next_page = url_for('main.index')
|
||||||
|
response = make_response(redirect(next_page))
|
||||||
|
if input.low_bandwidth_mode.data:
|
||||||
|
response.set_cookie('low_bandwidth', '1', expires=datetime(year=2099, month=12, day=30))
|
||||||
|
else:
|
||||||
|
response.set_cookie('low_bandwidth', '0', expires=datetime(year=2099, month=12, day=30))
|
||||||
|
return response
|
||||||
|
elif src == SRC_API:
|
||||||
|
token = user.encode_jwt_token()
|
||||||
|
if token:
|
||||||
|
login_json = {
|
||||||
|
"jwt": token,
|
||||||
|
"registration_created": user.verified,
|
||||||
|
"verify_email_sent": True
|
||||||
|
}
|
||||||
|
return login_json
|
||||||
|
else:
|
||||||
|
raise Exception('could_not_generate_token')
|
94
app/shared/post.py
Normal file
94
app/shared/post.py
Normal file
|
@ -0,0 +1,94 @@
|
||||||
|
from app import cache
|
||||||
|
from app.activitypub.signature import default_context, post_request_in_background
|
||||||
|
from app.community.util import send_to_remote_instance
|
||||||
|
from app.models import Post, User
|
||||||
|
from app.utils import gibberish, instance_banned, render_template, authorise_api_user, recently_upvoted_posts, recently_downvoted_posts
|
||||||
|
|
||||||
|
from flask import current_app, request
|
||||||
|
from flask_login import current_user
|
||||||
|
|
||||||
|
|
||||||
|
# would be in app/constants.py
|
||||||
|
SRC_WEB = 1
|
||||||
|
SRC_PUB = 2
|
||||||
|
SRC_API = 3
|
||||||
|
|
||||||
|
# function can be shared between WEB and API (only API calls it for now)
|
||||||
|
# post_vote in app/post/routes would just need to do 'return vote_for_post(post_id, vote_direction, SRC_WEB)'
|
||||||
|
|
||||||
|
def vote_for_post(post_id: int, vote_direction, src, auth=None):
|
||||||
|
if src == SRC_API and auth is not None:
|
||||||
|
post = Post.query.get(post_id)
|
||||||
|
if not post:
|
||||||
|
raise Exception('post_not_found')
|
||||||
|
try:
|
||||||
|
user = authorise_api_user(auth, return_type='model')
|
||||||
|
except:
|
||||||
|
raise
|
||||||
|
else:
|
||||||
|
post = Post.query.get_or_404(post_id)
|
||||||
|
user = current_user
|
||||||
|
|
||||||
|
undo = post.vote(user, vote_direction)
|
||||||
|
|
||||||
|
if not post.community.local_only:
|
||||||
|
if undo:
|
||||||
|
action_json = {
|
||||||
|
'actor': user.public_url(not(post.community.instance.votes_are_public() and user.vote_privately())),
|
||||||
|
'type': 'Undo',
|
||||||
|
'id': f"https://{current_app.config['SERVER_NAME']}/activities/undo/{gibberish(15)}",
|
||||||
|
'audience': post.community.public_url(),
|
||||||
|
'object': {
|
||||||
|
'actor': user.public_url(not(post.community.instance.votes_are_public() and user.vote_privately())),
|
||||||
|
'object': post.public_url(),
|
||||||
|
'type': undo,
|
||||||
|
'id': f"https://{current_app.config['SERVER_NAME']}/activities/{undo.lower()}/{gibberish(15)}",
|
||||||
|
'audience': post.community.public_url()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
action_type = 'Like' if vote_direction == 'upvote' else 'Dislike'
|
||||||
|
action_json = {
|
||||||
|
'actor': user.public_url(not(post.community.instance.votes_are_public() and user.vote_privately())),
|
||||||
|
'object': post.profile_id(),
|
||||||
|
'type': action_type,
|
||||||
|
'id': f"https://{current_app.config['SERVER_NAME']}/activities/{action_type.lower()}/{gibberish(15)}",
|
||||||
|
'audience': post.community.public_url()
|
||||||
|
}
|
||||||
|
if post.community.is_local():
|
||||||
|
announce = {
|
||||||
|
"id": f"https://{current_app.config['SERVER_NAME']}/activities/announce/{gibberish(15)}",
|
||||||
|
"type": 'Announce',
|
||||||
|
"to": [
|
||||||
|
"https://www.w3.org/ns/activitystreams#Public"
|
||||||
|
],
|
||||||
|
"actor": post.community.public_url(),
|
||||||
|
"cc": [
|
||||||
|
post.community.ap_followers_url
|
||||||
|
],
|
||||||
|
'@context': default_context(),
|
||||||
|
'object': action_json
|
||||||
|
}
|
||||||
|
for instance in post.community.following_instances():
|
||||||
|
if instance.inbox and not user.has_blocked_instance(instance.id) and not instance_banned(instance.domain):
|
||||||
|
send_to_remote_instance(instance.id, post.community.id, announce)
|
||||||
|
else:
|
||||||
|
post_request_in_background(post.community.ap_inbox_url, action_json, user.private_key,
|
||||||
|
user.public_url(not(post.community.instance.votes_are_public() and user.vote_privately())) + '#main-key')
|
||||||
|
|
||||||
|
|
||||||
|
if src == SRC_API:
|
||||||
|
return user.id
|
||||||
|
else:
|
||||||
|
recently_upvoted = []
|
||||||
|
recently_downvoted = []
|
||||||
|
if vote_direction == 'upvote' and undo is None:
|
||||||
|
recently_upvoted = [post_id]
|
||||||
|
elif vote_direction == 'downvote' and undo is None:
|
||||||
|
recently_downvoted = [post_id]
|
||||||
|
cache.delete_memoized(recently_upvoted_posts, user.id)
|
||||||
|
cache.delete_memoized(recently_downvoted_posts, user.id)
|
||||||
|
|
||||||
|
template = 'post/_post_voting_buttons.html' if request.args.get('style', '') == '' else 'post/_post_voting_buttons_masonry.html'
|
||||||
|
return render_template(template, post=post, community=post.community, recently_upvoted=recently_upvoted,
|
||||||
|
recently_downvoted=recently_downvoted)
|
97
app/shared/reply.py
Normal file
97
app/shared/reply.py
Normal file
|
@ -0,0 +1,97 @@
|
||||||
|
from app import cache
|
||||||
|
from app.activitypub.signature import default_context, post_request_in_background
|
||||||
|
from app.community.util import send_to_remote_instance
|
||||||
|
from app.models import PostReply, User
|
||||||
|
from app.utils import gibberish, instance_banned, render_template, authorise_api_user, recently_upvoted_post_replies, recently_downvoted_post_replies
|
||||||
|
|
||||||
|
from flask import current_app, request
|
||||||
|
from flask_login import current_user
|
||||||
|
|
||||||
|
|
||||||
|
# would be in app/constants.py
|
||||||
|
SRC_WEB = 1
|
||||||
|
SRC_PUB = 2
|
||||||
|
SRC_API = 3
|
||||||
|
|
||||||
|
# function can be shared between WEB and API (only API calls it for now)
|
||||||
|
# comment_vote in app/post/routes would just need to do 'return vote_for_reply(reply_id, vote_direction, SRC_WEB)'
|
||||||
|
|
||||||
|
def vote_for_reply(reply_id: int, vote_direction, src, auth=None):
|
||||||
|
if src == SRC_API and auth is not None:
|
||||||
|
reply = PostReply.query.get(reply_id)
|
||||||
|
if not reply:
|
||||||
|
raise Exception('reply_not_found')
|
||||||
|
try:
|
||||||
|
user = authorise_api_user(auth, return_type='model')
|
||||||
|
except:
|
||||||
|
raise
|
||||||
|
else:
|
||||||
|
reply = PostReply.query.get_or_404(post_id)
|
||||||
|
user = current_user
|
||||||
|
|
||||||
|
undo = reply.vote(user, vote_direction)
|
||||||
|
|
||||||
|
if not reply.community.local_only:
|
||||||
|
if undo:
|
||||||
|
action_json = {
|
||||||
|
'actor': user.public_url(not(reply.community.instance.votes_are_public() and user.vote_privately())),
|
||||||
|
'type': 'Undo',
|
||||||
|
'id': f"https://{current_app.config['SERVER_NAME']}/activities/undo/{gibberish(15)}",
|
||||||
|
'audience': reply.community.public_url(),
|
||||||
|
'object': {
|
||||||
|
'actor': user.public_url(not(reply.community.instance.votes_are_public() and user.vote_privately())),
|
||||||
|
'object': reply.public_url(),
|
||||||
|
'type': undo,
|
||||||
|
'id': f"https://{current_app.config['SERVER_NAME']}/activities/{undo.lower()}/{gibberish(15)}",
|
||||||
|
'audience': reply.community.public_url()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
action_type = 'Like' if vote_direction == 'upvote' else 'Dislike'
|
||||||
|
action_json = {
|
||||||
|
'actor': user.public_url(not(reply.community.instance.votes_are_public() and user.vote_privately())),
|
||||||
|
'object': reply.public_url(),
|
||||||
|
'type': action_type,
|
||||||
|
'id': f"https://{current_app.config['SERVER_NAME']}/activities/{action_type.lower()}/{gibberish(15)}",
|
||||||
|
'audience': reply.community.public_url()
|
||||||
|
}
|
||||||
|
if reply.community.is_local():
|
||||||
|
announce = {
|
||||||
|
"id": f"https://{current_app.config['SERVER_NAME']}/activities/announce/{gibberish(15)}",
|
||||||
|
"type": 'Announce',
|
||||||
|
"to": [
|
||||||
|
"https://www.w3.org/ns/activitystreams#Public"
|
||||||
|
],
|
||||||
|
"actor": reply.community.ap_profile_id,
|
||||||
|
"cc": [
|
||||||
|
reply.community.ap_followers_url
|
||||||
|
],
|
||||||
|
'@context': default_context(),
|
||||||
|
'object': action_json
|
||||||
|
}
|
||||||
|
for instance in reply.community.following_instances():
|
||||||
|
if instance.inbox and not user.has_blocked_instance(instance.id) and not instance_banned(instance.domain):
|
||||||
|
send_to_remote_instance(instance.id, reply.community.id, announce)
|
||||||
|
else:
|
||||||
|
post_request_in_background(reply.community.ap_inbox_url, action_json, user.private_key,
|
||||||
|
user.public_url(not(reply.community.instance.votes_are_public() and user.vote_privately())) + '#main-key')
|
||||||
|
|
||||||
|
if src == SRC_API:
|
||||||
|
return user.id
|
||||||
|
else:
|
||||||
|
recently_upvoted = []
|
||||||
|
recently_downvoted = []
|
||||||
|
if vote_direction == 'upvote' and undo is None:
|
||||||
|
recently_upvoted = [reply_id]
|
||||||
|
elif vote_direction == 'downvote' and undo is None:
|
||||||
|
recently_downvoted = [reply_id]
|
||||||
|
cache.delete_memoized(recently_upvoted_post_replies, user.id)
|
||||||
|
cache.delete_memoized(recently_downvoted_post_replies, user.id)
|
||||||
|
|
||||||
|
return render_template('post/_reply_voting_buttons.html', comment=reply,
|
||||||
|
recently_upvoted_replies=recently_upvoted,
|
||||||
|
recently_downvoted_replies=recently_downvoted,
|
||||||
|
community=reply.community)
|
||||||
|
|
||||||
|
|
||||||
|
|
21
app/utils.py
21
app/utils.py
|
@ -19,6 +19,7 @@ from functools import wraps
|
||||||
import flask
|
import flask
|
||||||
from bs4 import BeautifulSoup, MarkupResemblesLocatorWarning
|
from bs4 import BeautifulSoup, MarkupResemblesLocatorWarning
|
||||||
import warnings
|
import warnings
|
||||||
|
import jwt
|
||||||
|
|
||||||
|
|
||||||
warnings.filterwarnings("ignore", category=MarkupResemblesLocatorWarning)
|
warnings.filterwarnings("ignore", category=MarkupResemblesLocatorWarning)
|
||||||
|
@ -1261,3 +1262,23 @@ def add_to_modlog_activitypub(action: str, actor: User, community_id: int = None
|
||||||
db.session.add(ModLog(user_id=actor.id, community_id=community_id, type=action_type, action=action,
|
db.session.add(ModLog(user_id=actor.id, community_id=community_id, type=action_type, action=action,
|
||||||
reason=reason, link=link, link_text=link_text, public=get_setting('public_modlog', False)))
|
reason=reason, link=link, link_text=link_text, public=get_setting('public_modlog', False)))
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
|
|
||||||
|
def authorise_api_user(auth, return_type='id'):
|
||||||
|
token = auth[7:] # remove 'Bearer '
|
||||||
|
|
||||||
|
try:
|
||||||
|
decoded = jwt.decode(token, current_app.config['SECRET_KEY'], algorithms=["HS256"])
|
||||||
|
if decoded:
|
||||||
|
user_id = decoded['sub']
|
||||||
|
issued_at = decoded['iat'] # use to check against blacklisted JWTs
|
||||||
|
user = User.query.filter_by(id=user_id, ap_id=None, verified=True, banned=False, deleted=False).scalar()
|
||||||
|
if user:
|
||||||
|
if return_type == 'model':
|
||||||
|
return user
|
||||||
|
else:
|
||||||
|
return user_id
|
||||||
|
else:
|
||||||
|
raise Exception('incorrect_login')
|
||||||
|
except jwt.InvalidTokenError:
|
||||||
|
raise Exception('invalid_token')
|
||||||
|
|
Loading…
Reference in a new issue