mirror of
https://codeberg.org/rimu/pyfedi
synced 2025-01-23 19:36:56 -08:00
Merge remote-tracking branch 'origin/main'
This commit is contained in:
commit
91465c4ced
6 changed files with 205 additions and 12 deletions
|
@ -2,7 +2,7 @@ from app.api.alpha import bp
|
|||
from app.api.alpha.utils import get_site, post_site_block, \
|
||||
get_search, \
|
||||
get_post_list, get_post, post_post_like, put_post_save, put_post_subscribe, \
|
||||
get_reply_list, post_reply_like, put_reply_save, put_reply_subscribe, \
|
||||
get_reply_list, post_reply_like, put_reply_save, put_reply_subscribe, post_reply, \
|
||||
get_community_list, get_community, post_community_follow, post_community_block, \
|
||||
get_user, post_user_block
|
||||
from app.shared.auth import log_user_in
|
||||
|
@ -206,6 +206,18 @@ def put_alpha_comment_subscribe():
|
|||
return jsonify({"error": str(ex)}), 400
|
||||
|
||||
|
||||
@bp.route('/api/alpha/comment', methods=['POST'])
|
||||
def post_alpha_comment():
|
||||
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(auth, data))
|
||||
except Exception as ex:
|
||||
return jsonify({"error": str(ex)}), 400
|
||||
|
||||
|
||||
# User
|
||||
@bp.route('/api/alpha/user', methods=['GET'])
|
||||
def get_alpha_user():
|
||||
|
@ -287,7 +299,6 @@ def alpha_post():
|
|||
# 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'])
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
from app.api.alpha.utils.site import get_site, post_site_block
|
||||
from app.api.alpha.utils.misc import get_search
|
||||
from app.api.alpha.utils.post import get_post_list, get_post, post_post_like, put_post_save, put_post_subscribe
|
||||
from app.api.alpha.utils.reply import get_reply_list, post_reply_like, put_reply_save, put_reply_subscribe
|
||||
from app.api.alpha.utils.reply import get_reply_list, post_reply_like, put_reply_save, put_reply_subscribe, post_reply
|
||||
from app.api.alpha.utils.community import get_community, get_community_list, post_community_follow, post_community_block
|
||||
from app.api.alpha.utils.user import get_user, post_user_block
|
||||
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
from app import cache
|
||||
from app.api.alpha.utils.validators import required, integer_expected, boolean_expected
|
||||
from app.api.alpha.utils.validators import required, integer_expected, boolean_expected, string_expected
|
||||
from app.api.alpha.views import reply_view
|
||||
from app.models import PostReply
|
||||
from app.shared.reply import vote_for_reply, bookmark_the_post_reply, remove_the_bookmark_from_post_reply, toggle_post_reply_notification
|
||||
from app.models import PostReply, Post
|
||||
from app.shared.reply import vote_for_reply, bookmark_the_post_reply, remove_the_bookmark_from_post_reply, toggle_post_reply_notification, make_reply
|
||||
from app.utils import authorise_api_user, blocked_users, blocked_instances
|
||||
|
||||
from sqlalchemy import desc
|
||||
|
@ -115,3 +115,24 @@ def put_reply_subscribe(auth, data):
|
|||
user_id = toggle_post_reply_notification(reply_id, SRC_API, auth)
|
||||
reply_json = reply_view(reply=reply_id, variant=4, user_id=user_id)
|
||||
return reply_json
|
||||
|
||||
|
||||
def post_reply(auth,data):
|
||||
required(['body', 'post_id'], data)
|
||||
string_expected(['body',], data)
|
||||
integer_expected(['post_id', 'parent_id', 'language_id'], data)
|
||||
|
||||
body = data['body']
|
||||
post_id = data['post_id']
|
||||
parent_id = data['parent_id'] if 'parent_id' in data else None
|
||||
language_id = data['language_id'] if 'language_id' in data else 2
|
||||
|
||||
input = {'body': body, 'notify_author': True, 'language_id': language_id}
|
||||
post = Post.query.get(post_id)
|
||||
if not post:
|
||||
raise Exception('parent_not_found')
|
||||
|
||||
user_id, reply = make_reply(input, post, parent_id, SRC_API, auth)
|
||||
|
||||
reply_json = reply_view(reply=reply, variant=4, user_id=user_id)
|
||||
return reply_json
|
||||
|
|
|
@ -85,7 +85,7 @@ class Instance(db.Model):
|
|||
def votes_are_public(self):
|
||||
if self.trusted is True: # only vote privately with untrusted instances
|
||||
return False
|
||||
return self.software.lower() == 'lemmy' or self.software.lower() == 'mbin' or self.software.lower() == 'kbin'
|
||||
return self.software.lower() == 'lemmy' or self.software.lower() == 'mbin' or self.software.lower() == 'kbin' or self.software.lower() == 'guppe groups'
|
||||
|
||||
def post_count(self):
|
||||
return db.session.execute(text('SELECT COUNT(id) as c FROM "post" WHERE instance_id = :instance_id'),
|
||||
|
|
|
@ -324,7 +324,11 @@ def post_vote(post_id: int, vote_direction):
|
|||
if instance.inbox and not current_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, current_user.private_key,
|
||||
inbox = post.community.ap_inbox_url
|
||||
if (post.community.ap_domain and post.author.ap_inbox_url and # sanity check these fields aren't null
|
||||
post.community.ap_domain == 'a.gup.pe' and vote_direction == 'upvote'): # send upvotes to post author's instance instead of a.gup.pe (who reject them)
|
||||
inbox = post.author.ap_inbox_url
|
||||
post_request_in_background(inbox, action_json, current_user.private_key,
|
||||
current_user.public_url(not(post.community.instance.votes_are_public() and current_user.vote_privately())) + '#main-key')
|
||||
|
||||
recently_upvoted = []
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
from app import cache, db
|
||||
from app.activitypub.signature import default_context, post_request_in_background
|
||||
from app.activitypub.signature import default_context, post_request_in_background, post_request
|
||||
from app.community.util import send_to_remote_instance
|
||||
from app.constants import *
|
||||
from app.models import NotificationSubscription, PostReply, PostReplyBookmark, User
|
||||
from app.utils import gibberish, instance_banned, render_template, authorise_api_user, recently_upvoted_post_replies, recently_downvoted_post_replies, shorten_string
|
||||
from app.models import NotificationSubscription, PostReply, PostReplyBookmark, User, utcnow
|
||||
from app.utils import gibberish, instance_banned, render_template, authorise_api_user, recently_upvoted_post_replies, recently_downvoted_post_replies, shorten_string, \
|
||||
piefed_markdown_to_lemmy_markdown, markdown_to_html, ap_datetime
|
||||
|
||||
from flask import abort, current_app, flash, redirect, request, url_for
|
||||
from flask_babel import _
|
||||
|
@ -175,7 +176,7 @@ def toggle_post_reply_notification(post_reply_id: int, src, auth=None):
|
|||
db.session.commit()
|
||||
else: # no subscription yet, so make one
|
||||
new_notification = NotificationSubscription(name=shorten_string(_('Replies to my comment on %(post_title)s',
|
||||
post_title=post_reply.post.title)), user_id=user_id, entity_id=post_reply.id,
|
||||
post_title=post_reply.post.title)), user_id=user_id, entity_id=post_reply.id,
|
||||
type=NOTIF_REPLY)
|
||||
db.session.add(new_notification)
|
||||
db.session.commit()
|
||||
|
@ -184,3 +185,159 @@ def toggle_post_reply_notification(post_reply_id: int, src, auth=None):
|
|||
return user_id
|
||||
else:
|
||||
return render_template('post/_reply_notification_toggle.html', comment={'comment': post_reply})
|
||||
|
||||
|
||||
# there are undoubtedly better algos for this
|
||||
def basic_rate_limit_check(user):
|
||||
weeks_active = int((utcnow() - user.created).days / 7)
|
||||
score = user.post_reply_count * weeks_active
|
||||
|
||||
if score > 100:
|
||||
score = 10
|
||||
else:
|
||||
score = int(score/10)
|
||||
|
||||
# a user with a 10-week old account, who has made 10 replies, will score 10, so their rate limit will be 0
|
||||
# a user with a new account, and/or has made zero replies, will score 0 (so will have to wait 10 minutes between each new comment)
|
||||
# other users will score from 1-9, so their rate limits will be between 9 and 1 minutes.
|
||||
|
||||
rate_limit = (10-score)*60
|
||||
|
||||
recent_reply = cache.get(f'{user.id} has recently replied')
|
||||
if not recent_reply:
|
||||
cache.set(f'{user.id} has recently replied', True, timeout=rate_limit)
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
def make_reply(input, post, parent_id, src, auth=None):
|
||||
if src == SRC_API:
|
||||
user = authorise_api_user(auth, return_type='model')
|
||||
if not basic_rate_limit_check(user):
|
||||
raise Exception('rate_limited')
|
||||
content = input['body']
|
||||
notify_author = input['notify_author']
|
||||
language_id = input['language_id']
|
||||
else:
|
||||
user = current_user
|
||||
content = input.body.data
|
||||
notify_author = input.notify_author.data
|
||||
language_id = input.language_id.data
|
||||
|
||||
if parent_id:
|
||||
parent_reply = PostReply.query.get(parent_id)
|
||||
if not parent_reply:
|
||||
raise Exception('parent_reply_not_found')
|
||||
else:
|
||||
parent_reply = None
|
||||
|
||||
|
||||
# WEBFORM would call 'make_reply' in a try block, so any exception from 'new' would bubble-up for it to handle
|
||||
reply = PostReply.new(user, post, in_reply_to=parent_reply, body=piefed_markdown_to_lemmy_markdown(content),
|
||||
body_html=markdown_to_html(content), notify_author=notify_author,
|
||||
language_id=language_id)
|
||||
|
||||
user.language_id = language_id
|
||||
reply.ap_id = reply.profile_id()
|
||||
db.session.commit()
|
||||
|
||||
# federation
|
||||
if parent_id:
|
||||
in_reply_to = parent_reply
|
||||
else:
|
||||
in_reply_to = post
|
||||
|
||||
if not post.community.local_only:
|
||||
reply_json = {
|
||||
'type': 'Note',
|
||||
'id': reply.public_url(),
|
||||
'attributedTo': user.public_url(),
|
||||
'to': [
|
||||
'https://www.w3.org/ns/activitystreams#Public'
|
||||
],
|
||||
'cc': [
|
||||
post.community.public_url(),
|
||||
in_reply_to.author.public_url()
|
||||
],
|
||||
'content': reply.body_html,
|
||||
'inReplyTo': in_reply_to.profile_id(),
|
||||
'url': reply.profile_id(),
|
||||
'mediaType': 'text/html',
|
||||
'source': {'content': reply.body, 'mediaType': 'text/markdown'},
|
||||
'published': ap_datetime(utcnow()),
|
||||
'distinguished': False,
|
||||
'audience': post.community.public_url(),
|
||||
'contentMap': {
|
||||
'en': reply.body_html
|
||||
}
|
||||
}
|
||||
create_json = {
|
||||
'@context': default_context(),
|
||||
'type': 'Create',
|
||||
'actor': user.public_url(),
|
||||
'audience': post.community.public_url(),
|
||||
'to': [
|
||||
'https://www.w3.org/ns/activitystreams#Public'
|
||||
],
|
||||
'cc': [
|
||||
post.community.public_url(),
|
||||
in_reply_to.author.public_url()
|
||||
],
|
||||
'object': reply_json,
|
||||
'id': f"https://{current_app.config['SERVER_NAME']}/activities/create/{gibberish(15)}"
|
||||
}
|
||||
if in_reply_to.notify_author and in_reply_to.author.ap_id is not None:
|
||||
reply_json['tag'] = [
|
||||
{
|
||||
'href': in_reply_to.author.public_url(),
|
||||
'name': in_reply_to.author.mention_tag(),
|
||||
'type': 'Mention'
|
||||
}
|
||||
]
|
||||
create_json['tag'] = [
|
||||
{
|
||||
'href': in_reply_to.author.public_url(),
|
||||
'name': in_reply_to.author.mention_tag(),
|
||||
'type': 'Mention'
|
||||
}
|
||||
]
|
||||
if not post.community.is_local(): # this is a remote community, send it to the instance that hosts it
|
||||
success = post_request(post.community.ap_inbox_url, create_json, user.private_key,
|
||||
user.public_url() + '#main-key')
|
||||
if src != SRC_API:
|
||||
if success is False or isinstance(success, str):
|
||||
flash('Failed to send reply', 'error')
|
||||
else: # local community - send it to followers on remote instances
|
||||
del create_json['@context']
|
||||
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': create_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)
|
||||
|
||||
# send copy of Note to comment author (who won't otherwise get it if no-one else on their instance is subscribed to the community)
|
||||
if not in_reply_to.author.is_local() and in_reply_to.author.ap_domain != reply.community.ap_domain:
|
||||
if not post.community.is_local() or (post.community.is_local and not post.community.has_followers_from_domain(in_reply_to.author.ap_domain)):
|
||||
success = post_request(in_reply_to.author.ap_inbox_url, create_json, user.private_key, user.public_url() + '#main-key')
|
||||
if success is False or isinstance(success, str):
|
||||
# sending to shared inbox is good enough for Mastodon, but Lemmy will reject it the local community has no followers
|
||||
personal_inbox = in_reply_to.author.public_url() + '/inbox'
|
||||
post_request(personal_inbox, create_json, user.private_key, user.public_url() + '#main-key')
|
||||
|
||||
|
||||
if src == SRC_API:
|
||||
return user.id, reply
|
||||
else:
|
||||
return reply
|
||||
|
|
Loading…
Add table
Reference in a new issue