From 721383ff862820e58a4cd5b3e9a64152a0f0c829 Mon Sep 17 00:00:00 2001 From: Hendrik L Date: Mon, 6 Jan 2025 11:23:35 +0100 Subject: [PATCH 01/16] fix admin users table --- app/templates/admin/users.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/templates/admin/users.html b/app/templates/admin/users.html index 02c92913..03a3344d 100644 --- a/app/templates/admin/users.html +++ b/app/templates/admin/users.html @@ -80,8 +80,8 @@ {{ 'Banned'|safe if user.banned }} {{ 'Banned posts'|safe if user.ban_posts }} {{ 'Banned comments'|safe if user.ban_comments }} - {{ arrow.get(user.last_seen).humanize(locale=locale) }} {{ user.reports if user.reports > 0 }} + {% if user.attitude %}{{ (user.attitude * 100) | round | int }}%{% endif %} {% if user.reputation %}R {{ user.reputation | round | int }}{% endif %} {{ arrow.get(user.last_seen).humanize(locale=locale) }} Edit, From 89ce75e051569acc54dbefab9279367bc3b56207 Mon Sep 17 00:00:00 2001 From: freamon Date: Mon, 6 Jan 2025 11:20:32 +0000 Subject: [PATCH 02/16] Also list moderated / owned communities in 'Joined' tab --- app/main/routes.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/main/routes.py b/app/main/routes.py index e487800d..89c78b51 100644 --- a/app/main/routes.py +++ b/app/main/routes.py @@ -290,10 +290,13 @@ def list_subscribed_communities(): all_communities = Community.query.filter_by(banned=False) # get the user's joined communities user_joined_communities = joined_communities(current_user.id) + user_moderating_communities = moderating_communities(current_user.id) # get the joined community ids list joined_ids = [] for jc in user_joined_communities: joined_ids.append(jc.id) + for mc in user_moderating_communities: + joined_ids.append(mc.id) # filter down to just the joined communities communities = all_communities.filter(Community.id.in_(joined_ids)) From 45864777402ff770b6f7ea0f117c1d6618e75195 Mon Sep 17 00:00:00 2001 From: freamon Date: Sun, 5 Jan 2025 17:29:08 +0000 Subject: [PATCH 03/16] API: return 'moderates' and 'follows' from logged-in Site response --- app/api/alpha/utils/site.py | 79 +++++++++++------ app/api/alpha/views.py | 3 +- app/templates/themes/x_api/js/site.js | 120 ++++++++++++++++++++------ 3 files changed, 150 insertions(+), 52 deletions(-) diff --git a/app/api/alpha/utils/site.py b/app/api/alpha/utils/site.py index 980cfe6b..3ade562a 100644 --- a/app/api/alpha/utils/site.py +++ b/app/api/alpha/utils/site.py @@ -1,18 +1,65 @@ -from app import db +from app import cache, db from app.api.alpha.views import user_view, community_view, instance_view from app.api.alpha.utils.validators import required, integer_expected, boolean_expected from app.utils import authorise_api_user -from app.models import InstanceBlock, Language +from app.models import CommunityMember, InstanceBlock, Language from app.shared.site import block_remote_instance, unblock_remote_instance from flask import current_app, g from sqlalchemy import text +@cache.memoize(timeout=86400) 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() + +@cache.memoize(timeout=86400) +def moderating_communities(user): + cms = CommunityMember.query.filter_by(user_id=user.id, is_moderator=True) + moderates = [] + for cm in cms: + moderates.append({'community': community_view(cm.community_id, variant=1, stub=True), 'moderator': user_view(user, variant=1, stub=True)}) + return moderates + + +@cache.memoize(timeout=86400) +def joined_communities(user): + cms = CommunityMember.query.filter_by(user_id=user.id, is_banned=False) + follows = [] + for cm in cms: + follows.append({'community': community_view(cm.community_id, variant=1, stub=True), 'follower': user_view(user, variant=1, stub=True)}) + return follows + + +@cache.memoize(timeout=86400) +def blocked_people(user): + blocked = [] + blocked_ids = db.session.execute(text('SELECT blocked_id FROM "user_block" WHERE blocker_id = :blocker_id'), {"blocker_id": user.id}).scalars() + for blocked_id in blocked_ids: + blocked.append({'person': user_view(user, variant=1, stub=True), 'target': user_view(blocked_id, variant=1, stub=True)}) + return blocked + + +@cache.memoize(timeout=86400) +def blocked_communities(user): + blocked = [] + blocked_ids = db.session.execute(text('SELECT community_id FROM "community_block" WHERE user_id = :user_id'), {"user_id": user.id}).scalars() + for blocked_id in blocked_ids: + blocked.append({'person': user_view(user, variant=1, stub=True), 'community': community_view(blocked_id, variant=1, stub=True)}) + return blocked + + +@cache.memoize(timeout=86400) +def blocked_instances(user): + blocked = [] + blocked_ids = db.session.execute(text('SELECT instance_id FROM "instance_block" WHERE user_id = :user_id'), {"user_id": user.id}).scalars() + for blocked_id in blocked_ids: + blocked.append({'person': user_view(user, variant=1, stub=True), 'instance': instance_view(blocked_id, variant=1)}) + return blocked + + def get_site(auth): if auth: user = authorise_api_user(auth, return_type='model') @@ -74,31 +121,13 @@ def get_site(auth): "comment_count": user.post_reply_count } }, - #"moderates": [], - #"follows": [], - "community_blocks": [], - "instance_blocks": [], - "person_blocks": [], + "moderates": moderating_communities(user), + "follows": joined_communities(user), + "community_blocks": blocked_communities(user), + "instance_blocks": blocked_instances(user), + "person_blocks": blocked_people(user), "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)}) - """ - blocked_ids = db.session.execute(text('SELECT blocked_id FROM "user_block" WHERE blocker_id = :blocker_id'), {"blocker_id": user.id}).scalars() - for blocked_id in blocked_ids: - my_user['person_blocks'].append({'person': user_view(user, variant=1, stub=True), 'target': user_view(blocked_id, variant=1, stub=True)}) - blocked_ids = db.session.execute(text('SELECT community_id FROM "community_block" WHERE user_id = :user_id'), {"user_id": user.id}).scalars() - for blocked_id in blocked_ids: - my_user['community_blocks'].append({'person': user_view(user, variant=1, stub=True), 'community': community_view(blocked_id, variant=1, stub=True)}) - blocked_ids = db.session.execute(text('SELECT instance_id FROM "instance_block" WHERE user_id = :user_id'), {"user_id": user.id}).scalars() - for blocked_id in blocked_ids: - my_user['instance_blocks'].append({'person': user_view(user, variant=1, stub=True), 'instance': instance_view(blocked_id, variant=1)}) data = { "version": "1.0.0", "site": site diff --git a/app/api/alpha/views.py b/app/api/alpha/views.py index 107b15d7..3270ec94 100644 --- a/app/api/alpha/views.py +++ b/app/api/alpha/views.py @@ -173,7 +173,8 @@ def cached_community_view_variant_1(community: Community, stub=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}) + 'instance_id': community.instance_id if community.instance_id else 1, + 'ap_domain': community.ap_domain}) if community.description and not stub: v1['description'] = community.description if community.icon_id: diff --git a/app/templates/themes/x_api/js/site.js b/app/templates/themes/x_api/js/site.js index 2267c5cc..7c4b1993 100644 --- a/app/templates/themes/x_api/js/site.js +++ b/app/templates/themes/x_api/js/site.js @@ -14,36 +14,22 @@ if (session_jwt != null) { } export { jwt }; -const ul = document.getElementById('navbar_items'); +const navbar = document.getElementById('navbar_items'); if (jwt != null) { var request = {method: "GET", headers: {Authorization: `Bearer ${jwt}`}}; - ul.innerHTML = '' + - - '' + - - '' + - - ''; } else { var request = {method: "GET"}; - ul.innerHTML = '' + - '' + - '' + - ''; + navbar.innerHTML = '' + + '' + + '' + + ''; } fetch(api, request) @@ -60,6 +46,88 @@ fetch(api, request) // navbar document.querySelector('#navbar_title').innerHTML = 'Logo' + ' ' + data.site.name; + if (jwt != null) { + const all_communities_item = document.createElement('li'); + all_communities_item.innerHTML = 'All communities' + + const communities_menu = document.createElement('ul'); + communities_menu.className = 'dropdown-menu' + communities_menu.appendChild(all_communities_item) + + if (data.my_user.moderates.length > 0) { + const dropdown_divider = document.createElement('li'); + dropdown_divider.innerHTML = '' + communities_menu.appendChild(dropdown_divider) + const dropdown_header = document.createElement('li'); + dropdown_header.innerHTML = '' + communities_menu.appendChild(dropdown_header) + + for (let mods of data.my_user.moderates) { + let moderated_community_item = document.createElement('li'); + if (mods.community.local) { + moderated_community_item.innerHTML = '' + + mods.community.title + '' + ' (' + mods.community.ap_domain + ')' + + '' + } else { + moderated_community_item.innerHTML = '' + + mods.community.title + '' + ' (' + mods.community.ap_domain + ')' + + '' + } + communities_menu.appendChild(moderated_community_item) + } + } + + if (data.my_user.follows.length > 0) { + const dropdown_divider = document.createElement('li'); + dropdown_divider.innerHTML = '' + communities_menu.appendChild(dropdown_divider) + const dropdown_header = document.createElement('li'); + dropdown_header.innerHTML = '' + communities_menu.appendChild(dropdown_header) + + for (let follows of data.my_user.follows) { + let followed_community_item = document.createElement('li'); + if (follows.community.local) { + followed_community_item.innerHTML = '' + + follows.community.title + '' + ' (' + follows.community.ap_domain + ')' + + '' + } else { + followed_community_item.innerHTML = '' + + follows.community.title + '' + ' (' + follows.community.ap_domain + ')' + + '' + } + communities_menu.appendChild(followed_community_item) + } + } + + const communities_item = document.createElement('li') + communities_item.className = 'nav-item dropdown' + communities_item.innerHTML = '' + communities_item.appendChild(communities_menu) + navbar.appendChild(communities_item) + + + + /*const login_item = document.createElement('li') + login_item.className = 'nav-item' + login_item.innerHTML = 'Log in (via API)' + ul.appendChild(login_item) + + const communities_dropdown = document.createElement('li') + communities_dropdown.className = 'nav-item dropdown' + communities_dropdown.innerHTML = '' + const communities_dropdown_ul = document.createElement('ul') + communities_dropdown_ul.className = 'dropdown-menu' + const communities_dropdown_ul_item = document.createElement('li') + communities_dropdown_ul_item.className = 'dropdown-item' + communities_dropdown_ul_item.href = '/api/alpha/communities' + communities_dropdown_ul.appendChild(communities_dropdown_ul_item) + communities_dropdown.appendChild(communities_dropdown_ul) + ul.appendChild(communities_dropdown)*/ + } + // site info document.querySelector('#site_json').textContent = JSON.stringify(data, null, 2); }) From 618cdf4d443bb4e4bd4741f9951619d418283371 Mon Sep 17 00:00:00 2001 From: freamon Date: Mon, 16 Dec 2024 02:47:06 +0000 Subject: [PATCH 04/16] API: demo more responses using x-api theme --- app/templates/themes/x_api/base.html | 2 +- app/templates/themes/x_api/index.html | 8 +++- app/templates/themes/x_api/js/site.js | 53 ++++++++++++++++----------- 3 files changed, 39 insertions(+), 24 deletions(-) diff --git a/app/templates/themes/x_api/base.html b/app/templates/themes/x_api/base.html index d6daa031..74ecd2a0 100644 --- a/app/templates/themes/x_api/base.html +++ b/app/templates/themes/x_api/base.html @@ -40,7 +40,7 @@ - + {{ bootstrap.load_css() }} diff --git a/app/templates/themes/x_api/index.html b/app/templates/themes/x_api/index.html index 86595d80..495c2b83 100644 --- a/app/templates/themes/x_api/index.html +++ b/app/templates/themes/x_api/index.html @@ -1,6 +1,12 @@ {% extends 'themes/' + theme() + '/base.html' %} {% block app_content %} -

GET /api/alpha/site

+

JSON
+ +
+ +

+
JSON
+ {% endblock%} diff --git a/app/templates/themes/x_api/js/site.js b/app/templates/themes/x_api/js/site.js index 7c4b1993..35bfc629 100644 --- a/app/templates/themes/x_api/js/site.js +++ b/app/templates/themes/x_api/js/site.js @@ -1,6 +1,6 @@ const url = new URL(window.location.href); export const baseUrl = `${url.protocol}//${url.host}`; -const api = baseUrl + '/api/alpha/site'; +const api_site = baseUrl + '/api/alpha/site'; let jwt = null; let session_jwt = sessionStorage.getItem('jwt'); @@ -32,7 +32,7 @@ if (jwt != null) { ''; } -fetch(api, request) +fetch(api_site, request) .then(response => response.json()) .then(data => { // head @@ -41,7 +41,6 @@ fetch(api, request) document.querySelector('#icon_32').href = data.site.icon_32; document.querySelector('#icon_16').href = data.site.icon_16; document.querySelector('#icon_shortcut').href = data.site.icon_32; - document.querySelector('#favicon').href = baseUrl + '/static/images/favicon.ico'; // navbar document.querySelector('#navbar_title').innerHTML = 'Logo' + ' ' + data.site.name; @@ -106,29 +105,39 @@ fetch(api, request) communities_item.appendChild(communities_menu) navbar.appendChild(communities_item) + const user_settings_item = document.createElement('li') + user_settings_item.className = 'nav-item' + user_settings_item.innerHTML = 'User settings'; + navbar.appendChild(user_settings_item) - - /*const login_item = document.createElement('li') - login_item.className = 'nav-item' - login_item.innerHTML = 'Log in (via API)' - ul.appendChild(login_item) - - const communities_dropdown = document.createElement('li') - communities_dropdown.className = 'nav-item dropdown' - communities_dropdown.innerHTML = '' - const communities_dropdown_ul = document.createElement('ul') - communities_dropdown_ul.className = 'dropdown-menu' - const communities_dropdown_ul_item = document.createElement('li') - communities_dropdown_ul_item.className = 'dropdown-item' - communities_dropdown_ul_item.href = '/api/alpha/communities' - communities_dropdown_ul.appendChild(communities_dropdown_ul_item) - communities_dropdown.appendChild(communities_dropdown_ul) - ul.appendChild(communities_dropdown)*/ + const logout_item = document.createElement('li') + logout_item.className = 'nav-item' + logout_item.innerHTML = 'Log out (via API)'; + navbar.appendChild(logout_item) } // site info + if (jwt != null) { + document.querySelector('#site_request').innerHTML = 'GET /api/alpha/site [LOGGED IN]' + document.querySelector('#post_list_request').innerHTML = 'GET /api/alpha/post/list?type_=Subscribed&sort=New&page=1

' + } else { + document.querySelector('#site_request').innerHTML = 'GET /api/alpha/site [LOGGED OUT]' + document.querySelector('#post_list_request').innerHTML = 'GET /api/alpha/post/list?type_=Popular&sort=Hot&page=1

' + } + document.querySelector('#site_json').textContent = JSON.stringify(data, null, 2); }) + +if (jwt != null) { + var api_postlist = baseUrl + '/api/alpha/post/list?type_=Subscribed&sort=New&page=1'; +} else { + var api_postlist = baseUrl + '/api/alpha/post/list?type_=Popular&sort=Hot&page=1'; +} + +fetch(api_postlist, request) + .then(response => response.json()) + .then(data => { + document.querySelector('#post_list_json').textContent = JSON.stringify(data, null, 2); + + }) From 949a1c40a3558cce6c4d5b57f7406119f9cb8196 Mon Sep 17 00:00:00 2001 From: freamon Date: Mon, 6 Jan 2025 13:20:08 +0000 Subject: [PATCH 05/16] API: demo individual community response with x_api theme --- app/api/alpha/routes.py | 17 ++++++++ app/templates/themes/x_api/auth/login.html | 2 +- app/templates/themes/x_api/community.html | 12 ++++++ app/templates/themes/x_api/js/community.js | 26 +++++++++++++ app/templates/themes/x_api/js/site.js | 39 +++++++++++-------- .../themes/x_api/list_communities.html | 2 +- 6 files changed, 80 insertions(+), 18 deletions(-) create mode 100644 app/templates/themes/x_api/community.html create mode 100644 app/templates/themes/x_api/js/community.js diff --git a/app/api/alpha/routes.py b/app/api/alpha/routes.py index f14b7446..5d274d63 100644 --- a/app/api/alpha/routes.py +++ b/app/api/alpha/routes.py @@ -406,6 +406,7 @@ def alpha_emoji(): # HTML routes from flask import abort, render_template +from app.models import Community from app.utils import current_theme import os @@ -463,3 +464,19 @@ def get_alpha_communities(): return render_template(f'themes/{theme}/{template_name}') else: return render_template(template_name) + + +@bp.route('/api/alpha/c/', methods=['GET']) +def community_profile(actor): + if '@' in actor: + community = Community.query.filter_by(ap_id=actor.lower(), banned=False).first() + else: + community = Community.query.filter_by(name=actor, ap_id=None).first() + + template_name = "community.html" + + theme = current_theme() + if theme != '' and os.path.exists(f'app/templates/themes/{theme}/{template_name}'): + return render_template(f'themes/{theme}/{template_name}', community_id=community.id) + else: + return render_template(template_name, community_id=community.id) diff --git a/app/templates/themes/x_api/auth/login.html b/app/templates/themes/x_api/auth/login.html index b3798f35..18f8d20b 100644 --- a/app/templates/themes/x_api/auth/login.html +++ b/app/templates/themes/x_api/auth/login.html @@ -1,7 +1,7 @@ {% extends 'themes/' + theme() + '/base.html' %} {% block app_content %} -

GET /api/alpha/site

+

JSON

POST /api/alpha/user/login

diff --git a/app/templates/themes/x_api/community.html b/app/templates/themes/x_api/community.html new file mode 100644 index 00000000..63083251 --- /dev/null +++ b/app/templates/themes/x_api/community.html @@ -0,0 +1,12 @@ +{% extends 'themes/' + theme() + '/base.html' %} + +{% block app_content %} +

+
JSON
+

GET /api/alpha/community?id={{ community_id }}

+
JSON
+

GET /api/alpha/post/list?sort=Hot&page=1&community_id={{ community_id }}

+
JSON
+ + +{% endblock %} diff --git a/app/templates/themes/x_api/js/community.js b/app/templates/themes/x_api/js/community.js new file mode 100644 index 00000000..865d70cf --- /dev/null +++ b/app/templates/themes/x_api/js/community.js @@ -0,0 +1,26 @@ +const element = document.getElementById('community_request'); +const community_id = element.getAttribute('data-value'); + +import { baseUrl } from './site.js'; +const community_api = baseUrl + '/api/alpha/community?id=' + community_id; +const community_post_list_api = baseUrl + '/api/alpha/post/list?sort=Hot&page=1&community_id=' + community_id; + +import { jwt } from './site.js'; +if (jwt != null) { + var request = {method: "GET", headers: {Authorization: `Bearer ${jwt}`}}; +} else { + var request = {method: "GET"}; +} + +fetch(community_api, request) + .then(response => response.json()) + .then(data => { + document.querySelector('#community_json').textContent = JSON.stringify(data, null, 2); + }) + + +fetch(community_post_list_api, request) + .then(response => response.json()) + .then(data => { + document.querySelector('#community_post_list_json').textContent = JSON.stringify(data, null, 2); + }) diff --git a/app/templates/themes/x_api/js/site.js b/app/templates/themes/x_api/js/site.js index 35bfc629..4b3f3225 100644 --- a/app/templates/themes/x_api/js/site.js +++ b/app/templates/themes/x_api/js/site.js @@ -64,11 +64,11 @@ fetch(api_site, request) for (let mods of data.my_user.moderates) { let moderated_community_item = document.createElement('li'); if (mods.community.local) { - moderated_community_item.innerHTML = '' + + moderated_community_item.innerHTML = '' + mods.community.title + '' + ' (' + mods.community.ap_domain + ')' + '' } else { - moderated_community_item.innerHTML = '' + + moderated_community_item.innerHTML = '' + mods.community.title + '' + ' (' + mods.community.ap_domain + ')' + '' } @@ -87,11 +87,11 @@ fetch(api_site, request) for (let follows of data.my_user.follows) { let followed_community_item = document.createElement('li'); if (follows.community.local) { - followed_community_item.innerHTML = '' + + followed_community_item.innerHTML = '' + follows.community.title + '' + ' (' + follows.community.ap_domain + ')' + '' } else { - followed_community_item.innerHTML = '' + + followed_community_item.innerHTML = '' + follows.community.title + '' + ' (' + follows.community.ap_domain + ')' + '' } @@ -117,27 +117,34 @@ fetch(api_site, request) } // site info + let postlist = document.querySelector('#post_list_request') if (jwt != null) { document.querySelector('#site_request').innerHTML = 'GET /api/alpha/site [LOGGED IN]' - document.querySelector('#post_list_request').innerHTML = 'GET /api/alpha/post/list?type_=Subscribed&sort=New&page=1

' + if (postlist) { + postlist.innerHTML = 'GET /api/alpha/post/list?type_=Subscribed&sort=New&page=1

' + } } else { document.querySelector('#site_request').innerHTML = 'GET /api/alpha/site [LOGGED OUT]' - document.querySelector('#post_list_request').innerHTML = 'GET /api/alpha/post/list?type_=Popular&sort=Hot&page=1

' + if (postlist) { + postlist.innerHTML = 'GET /api/alpha/post/list?type_=Popular&sort=Hot&page=1

' + } } document.querySelector('#site_json').textContent = JSON.stringify(data, null, 2); }) -if (jwt != null) { - var api_postlist = baseUrl + '/api/alpha/post/list?type_=Subscribed&sort=New&page=1'; -} else { - var api_postlist = baseUrl + '/api/alpha/post/list?type_=Popular&sort=Hot&page=1'; -} - -fetch(api_postlist, request) - .then(response => response.json()) - .then(data => { - document.querySelector('#post_list_json').textContent = JSON.stringify(data, null, 2); +let postlist = document.querySelector('#post_list_request'); +if (postlist) { + if (jwt != null) { + var api_postlist = baseUrl + '/api/alpha/post/list?type_=Subscribed&sort=New&page=1'; + } else { + var api_postlist = baseUrl + '/api/alpha/post/list?type_=Popular&sort=Hot&page=1'; + } + fetch(api_postlist, request) + .then(response => response.json()) + .then(data => { + document.querySelector('#post_list_json').textContent = JSON.stringify(data, null, 2); }) +} diff --git a/app/templates/themes/x_api/list_communities.html b/app/templates/themes/x_api/list_communities.html index e078b37b..21b3df21 100644 --- a/app/templates/themes/x_api/list_communities.html +++ b/app/templates/themes/x_api/list_communities.html @@ -1,7 +1,7 @@ {% extends 'themes/' + theme() + '/base.html' %} {% block app_content %} -

GET /api/alpha/site

+

JSON

GET /api/alpha/community/list

JSON
From 26283a5d73458750b183109444766fddfcd55639 Mon Sep 17 00:00:00 2001 From: freamon Date: Mon, 6 Jan 2025 14:32:27 +0000 Subject: [PATCH 06/16] change 'find_community_ap_id()' to 'find_community()' --- app/activitypub/routes.py | 11 ++++------- app/activitypub/util.py | 18 ++++++++++-------- 2 files changed, 14 insertions(+), 15 deletions(-) diff --git a/app/activitypub/routes.py b/app/activitypub/routes.py index 9a13207f..da229c3c 100644 --- a/app/activitypub/routes.py +++ b/app/activitypub/routes.py @@ -25,7 +25,7 @@ from app.activitypub.util import public_key, users_total, active_half_year, acti update_post_from_activity, undo_vote, undo_downvote, post_to_page, get_redis_connection, find_reported_object, \ process_report, ensure_domains_match, can_edit, can_delete, remove_data_from_banned_user, resolve_remote_post, \ inform_followers_of_post_update, comment_model_to_json, restore_post_or_comment, ban_user, unban_user, \ - log_incoming_ap, find_community_ap_id, site_ban_remove_data, community_ban_remove_data + log_incoming_ap, find_community, site_ban_remove_data, community_ban_remove_data from app.utils import gibberish, get_setting, render_template, \ community_membership, ap_datetime, ip_address, can_downvote, \ can_upvote, can_create_post, awaken_dormant_instance, shorten_string, can_create_post_reply, sha256_digest, \ @@ -761,11 +761,10 @@ def process_inbox_request(request_json, store_ap_json): if post_being_replied_to.author.is_local(): inform_followers_of_post_update(post_being_replied_to.id, user.instance_id) return - community_ap_id = find_community_ap_id(request_json) + community = find_community(request_json) if not ensure_domains_match(request_json['object']): log_incoming_ap(id, APLOG_CREATE, APLOG_FAILURE, request_json if store_ap_json else None, 'Domains do not match') return - community = find_actor_or_create(community_ap_id, community_only=True, create_if_not_found=False) if community_ap_id else None if community and community.local_only: log_incoming_ap(id, APLOG_CREATE, APLOG_FAILURE, request_json if store_ap_json else None, 'Remote Create in local_only community') return @@ -835,8 +834,7 @@ def process_inbox_request(request_json, store_ap_json): if request_json['type'] == 'Add': # remote site is adding a local user as a moderator, and is sending directly rather than announcing (happens if not subscribed) mod = user - community_ap_id = find_community_ap_id(request_json) - community = find_actor_or_create(community_ap_id, community_only=True, create_if_not_found=False) if community_ap_id else None + community = find_community(request_json) if community: if not community.is_moderator(mod) and not community.is_instance_admin(mod): log_incoming_ap(id, APLOG_ADD, APLOG_FAILURE, request_json if store_ap_json else None, 'Does not have permission') @@ -866,8 +864,7 @@ def process_inbox_request(request_json, store_ap_json): if request_json['type'] == 'Remove': # remote site is removing a local user as a moderator, and is sending directly rather than announcing (happens if not subscribed) mod = user - community_ap_id = find_community_ap_id(request_json) - community = find_actor_or_create(community_ap_id, community_only=True, create_if_not_found=False) if community_ap_id else None + community = find_community(request_json) if community: if not community.is_moderator(mod) and not community.is_instance_admin(mod): log_incoming_ap(id, APLOG_ADD, APLOG_FAILURE, request_json if store_ap_json else None, 'Does not have permission') diff --git a/app/activitypub/util.py b/app/activitypub/util.py index 43e812af..7556db75 100644 --- a/app/activitypub/util.py +++ b/app/activitypub/util.py @@ -2535,7 +2535,7 @@ def log_incoming_ap(id, aplog_type, aplog_result, request_json, message=None): db.session.commit() -def find_community_ap_id(request_json): +def find_community(request_json): locations = ['audience', 'cc', 'to'] if 'object' in request_json and isinstance(request_json['object'], dict): rjs = [request_json, request_json['object']] @@ -2549,30 +2549,32 @@ def find_community_ap_id(request_json): if not potential_id.startswith('https://www.w3.org') and not potential_id.endswith('/followers'): potential_community = Community.query.filter_by(ap_profile_id=potential_id.lower()).first() if potential_community: - return potential_id + return potential_community if isinstance(potential_id, list): for c in potential_id: if not c.startswith('https://www.w3.org') and not c.endswith('/followers'): potential_community = Community.query.filter_by(ap_profile_id=c.lower()).first() if potential_community: - return c + return potential_community if not 'object' in request_json: return None if 'inReplyTo' in request_json['object'] and request_json['object']['inReplyTo'] is not None: - post_being_replied_to = Post.query.filter_by(ap_id=request_json['object']['inReplyTo']).first() + post_being_replied_to = Post.query.filter_by(ap_id=request_json['object']['inReplyTo'].lower()).first() if post_being_replied_to: - return post_being_replied_to.community.ap_profile_id + return post_being_replied_to.community else: - comment_being_replied_to = PostReply.query.filter_by(ap_id=request_json['object']['inReplyTo']).first() + comment_being_replied_to = PostReply.query.filter_by(ap_id=request_json['object']['inReplyTo'].lower()).first() if comment_being_replied_to: - return comment_being_replied_to.community.ap_profile_id + return comment_being_replied_to.community if request_json['object']['type'] == 'Video': # PeerTube if 'attributedTo' in request_json['object'] and isinstance(request_json['object']['attributedTo'], list): for a in request_json['object']['attributedTo']: if a['type'] == 'Group': - return a['id'] + potential_community = Community.query.filter_by(ap_profile_id=a['id'].lower()).first() + if potential_community: + return potential_community return None From af82bc707613f2ba57a66b537bf3ad36e6b63b64 Mon Sep 17 00:00:00 2001 From: freamon Date: Mon, 6 Jan 2025 19:13:23 +0000 Subject: [PATCH 07/16] Fetch and verify an object from its source if sent without a signature --- app/activitypub/routes.py | 32 +++++++++++++++----- app/activitypub/util.py | 62 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 86 insertions(+), 8 deletions(-) diff --git a/app/activitypub/routes.py b/app/activitypub/routes.py index da229c3c..8c5d2d56 100644 --- a/app/activitypub/routes.py +++ b/app/activitypub/routes.py @@ -25,7 +25,7 @@ from app.activitypub.util import public_key, users_total, active_half_year, acti update_post_from_activity, undo_vote, undo_downvote, post_to_page, get_redis_connection, find_reported_object, \ process_report, ensure_domains_match, can_edit, can_delete, remove_data_from_banned_user, resolve_remote_post, \ inform_followers_of_post_update, comment_model_to_json, restore_post_or_comment, ban_user, unban_user, \ - log_incoming_ap, find_community, site_ban_remove_data, community_ban_remove_data + log_incoming_ap, find_community, site_ban_remove_data, community_ban_remove_data, verify_object_from_source from app.utils import gibberish, get_setting, render_template, \ community_membership, ap_datetime, ip_address, can_downvote, \ can_upvote, can_create_post, awaken_dormant_instance, shorten_string, can_create_post_reply, sha256_digest, \ @@ -465,14 +465,19 @@ def shared_inbox(): HttpSignature.verify_request(request, actor.public_key, skip_date=True) except VerificationError as e: bounced = True - if not 'signature' in request_json: - log_incoming_ap(id, APLOG_NOTYPE, APLOG_FAILURE, request_json if store_ap_json else None, 'Could not verify HTTP signature: ' + str(e)) - return '', 400 # HTTP sig will fail if a.gup.pe or PeerTube have bounced a request, so check LD sig instead - try: - LDSignature.verify_signature(request_json, actor.public_key) - except VerificationError as e: - log_incoming_ap(id, APLOG_NOTYPE, APLOG_FAILURE, request_json if store_ap_json else None, 'Could not verify LD signature: ' + str(e)) + if 'signature' in request_json: + try: + LDSignature.verify_signature(request_json, actor.public_key) + except VerificationError as e: + log_incoming_ap(id, APLOG_NOTYPE, APLOG_FAILURE, request_json if store_ap_json else None, 'Could not verify LD signature: ' + str(e)) + return '', 400 + # not HTTP sig, and no LD sig, so reduce the inner object to just its remote ID, and then fetch it and check it in process_inbox_request() + elif ((request_json['type'] == 'Create' or request_json['type'] == 'Update') and + isinstance(request_json['object'], dict) and 'id' in request_json['object'] and isinstance(request_json['object']['id'], str)): + request_json['object'] = request_json['object']['id'] + else: + log_incoming_ap(id, APLOG_NOTYPE, APLOG_FAILURE, request_json if store_ap_json else None, 'Could not verify HTTP signature: ' + str(e)) return '', 400 actor.instance.last_seen = utcnow() @@ -565,6 +570,11 @@ def replay_inbox_request(request_json): process_delete_request(request_json, True) return + # testing verify_object_from_source() + if ((request_json['type'] == 'Create' or request_json['type'] == 'Update') and + isinstance(request_json['object'], dict) and 'id' in request_json['object'] and isinstance(request_json['object']['id'], str)): + request_json['object'] = request_json['object']['id'] + process_inbox_request(request_json, True) return @@ -706,6 +716,12 @@ def process_inbox_request(request_json, store_ap_json): # Create is new content. Update is often an edit, but Updates from Lemmy can also be new content if request_json['type'] == 'Create' or request_json['type'] == 'Update': + if isinstance(request_json['object'], str): + request_json = verify_object_from_source(request_json) # change request_json['object'] from str to dict, then process normally + if not request_json: + log_incoming_ap(id, APLOG_CREATE, APLOG_FAILURE, request_json if store_ap_json else None, 'Could not verify unsigned request from source') + return + if request_json['object']['type'] == 'ChatMessage': sender = user recipient_ap_id = request_json['object']['to'][0] diff --git a/app/activitypub/util.py b/app/activitypub/util.py index 7556db75..90079538 100644 --- a/app/activitypub/util.py +++ b/app/activitypub/util.py @@ -2467,6 +2467,68 @@ def resolve_remote_post_from_search(uri: str) -> Union[Post, None]: return None +# called from activitypub/routes if something is posted to us without any kind of signature (typically from PeerTube) +def verify_object_from_source(request_json): + uri = request_json['object'] + uri_domain = urlparse(uri).netloc + if not uri_domain: + return None + + create_domain = urlparse(request_json['actor']).netloc + if create_domain != uri_domain: + return None + + try: + object_request = get_request(uri, headers={'Accept': 'application/activity+json'}) + except httpx.HTTPError: + time.sleep(3) + try: + object_request = get_request(uri, headers={'Accept': 'application/activity+json'}) + except httpx.HTTPError: + return None + if object_request.status_code == 200: + try: + object = object_request.json() + except: + object_request.close() + return None + object_request.close() + elif object_request.status_code == 401: + try: + site = Site.query.get(1) + object_request = signed_get_request(uri, site.private_key, f"https://{current_app.config['SERVER_NAME']}/actor#main-key") + except httpx.HTTPError: + time.sleep(3) + try: + object_request = signed_get_request(uri, site.private_key, f"https://{current_app.config['SERVER_NAME']}/actor#main-key") + except httpx.HTTPError: + return None + try: + object = object_request.json() + except: + object_request.close() + return None + object_request.close() + else: + return None + + if not 'id' in object or not 'type' in object or not 'attributedTo' in object: + return None + + if isinstance(object['attributedTo'], str): + actor_domain = urlparse(object['attributedTo']).netloc + elif isinstance(object['attributedTo'], list) and 'id' in object['attributedTo']: + actor_domain = urlparse(object['attributedTo']['id']).netloc + else: + return None + + if uri_domain != actor_domain: + return None + + request_json['object'] = object + return request_json + + # This is for followers on microblog apps # Used to let them know a Poll has been updated with a new vote # The plan is to also use it for activities on local user's posts that aren't understood by being Announced (anything beyond the initial Create) From 7c817decf24cb01923e8696dbb6c10c1562baf40 Mon Sep 17 00:00:00 2001 From: freamon Date: Mon, 6 Jan 2025 22:54:41 +0000 Subject: [PATCH 08/16] misc bugfixes for PeerTube --- app/activitypub/util.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/activitypub/util.py b/app/activitypub/util.py index 90079538..6b0da9ff 100644 --- a/app/activitypub/util.py +++ b/app/activitypub/util.py @@ -1784,6 +1784,7 @@ def update_post_from_activity(post: Post, request_json: dict): if request_json['object']['type'] == 'Video': # return now for PeerTube, otherwise rest of this function breaks the post # consider querying the Likes endpoint (that mostly seems to be what Updates are about) + db.session.commit() return # Links @@ -2517,7 +2518,7 @@ def verify_object_from_source(request_json): if isinstance(object['attributedTo'], str): actor_domain = urlparse(object['attributedTo']).netloc - elif isinstance(object['attributedTo'], list) and 'id' in object['attributedTo']: + elif isinstance(object['attributedTo'], dict) and 'id' in object['attributedTo']: actor_domain = urlparse(object['attributedTo']['id']).netloc else: return None From 1fa74370c6bf55ddaa6bfb23c21c5f26d19fbd91 Mon Sep 17 00:00:00 2001 From: freamon Date: Tue, 7 Jan 2025 09:45:30 +0000 Subject: [PATCH 09/16] Move code to upvote own post into new() --- app/community/routes.py | 10 ---------- app/models.py | 9 +++++++-- 2 files changed, 7 insertions(+), 12 deletions(-) diff --git a/app/community/routes.py b/app/community/routes.py index d622c6b6..d1118dac 100644 --- a/app/community/routes.py +++ b/app/community/routes.py @@ -722,8 +722,6 @@ def add_post(actor, type): post.ap_id = f"https://{current_app.config['SERVER_NAME']}/post/{post.id}" db.session.commit() - upvote_own_post(post) - if post.type == POST_TYPE_POLL: poll = Poll.query.filter_by(post_id=post.id).first() if not poll.local_only: @@ -2007,11 +2005,3 @@ def check_url_already_posted(): abort(404) -def upvote_own_post(post): - post.score = 1 - post.up_votes = 1 - post.ranking = post.post_ranking(post.score, utcnow()) - vote = PostVote(user_id=current_user.id, post_id=post.id, author_id=current_user.id, effect=1) - db.session.add(vote) - db.session.commit() - cache.delete_memoized(recently_upvoted_posts, current_user.id) diff --git a/app/models.py b/app/models.py index c09a9745..43953841 100644 --- a/app/models.py +++ b/app/models.py @@ -1171,7 +1171,7 @@ class Post(db.Model): find_licence_or_create, make_image_sizes, notify_about_post from app.utils import allowlist_html, markdown_to_html, html_to_text, microblog_content_to_title, blocked_phrases, \ is_image_url, is_video_url, domain_from_url, opengraph_parse, shorten_string, remove_tracking_from_link, \ - is_video_hosting_site, communities_banned_from + is_video_hosting_site, communities_banned_from, recently_upvoted_posts microblog = False if 'name' not in request_json['object']: # Microblog posts @@ -1399,11 +1399,16 @@ class Post(db.Model): if post.community_id not in communities_banned_from(user.id): notify_about_post(post) + # attach initial upvote to author + vote = PostVote(user_id=user.id, post_id=post.id, author_id=user.id, effect=1) + db.session.add(vote) + if user.is_local(): + cache.delete_memoized(recently_upvoted_posts, user.id) if user.reputation > 100: post.up_votes += 1 post.score += 1 post.ranking = post.post_ranking(post.score, post.posted_at) - db.session.commit() + db.session.commit() return post From 78fd925282c469426c80401b19c85be82fa95d00 Mon Sep 17 00:00:00 2001 From: freamon Date: Tue, 7 Jan 2025 10:53:47 +0000 Subject: [PATCH 10/16] Add votes to posts from PeerTube instances when they send an update --- app/activitypub/routes.py | 5 ----- app/activitypub/util.py | 31 ++++++++++++++++++++++++++++++- 2 files changed, 30 insertions(+), 6 deletions(-) diff --git a/app/activitypub/routes.py b/app/activitypub/routes.py index 8c5d2d56..bf20f9e2 100644 --- a/app/activitypub/routes.py +++ b/app/activitypub/routes.py @@ -570,11 +570,6 @@ def replay_inbox_request(request_json): process_delete_request(request_json, True) return - # testing verify_object_from_source() - if ((request_json['type'] == 'Create' or request_json['type'] == 'Update') and - isinstance(request_json['object'], dict) and 'id' in request_json['object'] and isinstance(request_json['object']['id'], str)): - request_json['object'] = request_json['object']['id'] - process_inbox_request(request_json, True) return diff --git a/app/activitypub/util.py b/app/activitypub/util.py index 6b0da9ff..c0139e72 100644 --- a/app/activitypub/util.py +++ b/app/activitypub/util.py @@ -1782,8 +1782,37 @@ def update_post_from_activity(post: Post, request_json: dict): post.edited_at = utcnow() if request_json['object']['type'] == 'Video': + # fetching individual user details to attach to votes is probably too convoluted, so take the instance's word for it + upvotes = 1 # from OP + downvotes = 0 + endpoints = ['likes', 'dislikes'] + for endpoint in endpoints: + if endpoint in request_json['object']: + try: + object_request = get_request(request_json['object'][endpoint], headers={'Accept': 'application/activity+json'}) + except httpx.HTTPError: + time.sleep(3) + try: + object_request = get_request(request_json['object'][endpoint], headers={'Accept': 'application/activity+json'}) + except httpx.HTTPError: + object_request = None + if object_request and object_request.status_code == 200: + try: + object = object_request.json() + except: + object_request.close() + object = None + object_request.close() + if object and 'totalItems' in object: + if endpoint == 'likes': + upvotes += object['totalItems'] + if endpoint == 'dislikes': + downvotes += object['totalItems'] + post.up_votes = upvotes + post.down_votes = downvotes + post.score = upvotes - downvotes + post.ranking = post.post_ranking(post.score, post.posted_at) # return now for PeerTube, otherwise rest of this function breaks the post - # consider querying the Likes endpoint (that mostly seems to be what Updates are about) db.session.commit() return From 05bfed955ede87c2f051d096d6c631c408d647ce Mon Sep 17 00:00:00 2001 From: freamon Date: Tue, 7 Jan 2025 13:53:52 +0000 Subject: [PATCH 11/16] Streamline ap routes (part 01: likes) --- app/activitypub/routes.py | 71 +++++++++++++++++++++++---------------- 1 file changed, 42 insertions(+), 29 deletions(-) diff --git a/app/activitypub/routes.py b/app/activitypub/routes.py index bf20f9e2..fd19eecb 100644 --- a/app/activitypub/routes.py +++ b/app/activitypub/routes.py @@ -604,6 +604,40 @@ def process_inbox_request(request_json, store_ap_json): db.session.commit() community = None # found as needed + # Announce: take care of inner objects that are just a URL (PeerTube, a.gup.pe), or find the user if the inner object is a dict + if request_json['type'] == 'Announce': + if isinstance(request_json['object'], str): + if request_json['object'].startswith('https://' + current_app.config['SERVER_NAME']): + log_incoming_ap(id, APLOG_DUPLICATE, APLOG_IGNORED, request_json if store_ap_json else None, 'Activity about local content which is already present') + return + post = resolve_remote_post(request_json['object'], community.id, announce_actor=community.ap_profile_id, store_ap_json=store_ap_json) + if post: + log_incoming_ap(id, APLOG_ANNOUNCE, APLOG_SUCCESS, request_json) + else: + log_incoming_ap(id, APLOG_ANNOUNCE, APLOG_FAILURE, request_json, 'Could not resolve post') + return + + user_ap_id = request_json['object']['actor'] + user = find_actor_or_create(user_ap_id) + if not user or not isinstance(user, User): + log_incoming_ap(id, APLOG_ANNOUNCE, APLOG_FAILURE, request_json if store_ap_json else None, 'Blocked or unfound user for Announce object actor ' + user_ap_id) + return + + user.last_seen = site.last_active = utcnow() + user.instance.last_seen = utcnow() + user.instance.dormant = False + user.instance.gone_forever = False + user.instance.failures = 0 + db.session.commit() + + # Now that we have the community and the user from an Announce, we can save repeating code by removing it + # core_activity is checked for its Type, but request_json is passed to any other functions + announced = True + core_activity = request_json['object'] + else: + announced = False + core_activity = request_json + # Follow: remote user wants to join/follow one of our users or communities if request_json['type'] == 'Follow': target_ap_id = request_json['object'] @@ -822,8 +856,8 @@ def process_inbox_request(request_json, store_ap_json): log_incoming_ap(id, APLOG_DELETE, APLOG_FAILURE, request_json if store_ap_json else None, 'Delete: cannot find ' + ap_id) return - if request_json['type'] == 'Like' or request_json['type'] == 'EmojiReact': # Upvote - process_upvote(user, store_ap_json, request_json, announced=False) + if core_activity['type'] == 'Like' or core_activity['type'] == 'EmojiReact': # Upvote + process_upvote(user, store_ap_json, request_json, announced) return if request_json['type'] == 'Dislike': # Downvote @@ -1032,29 +1066,8 @@ def process_inbox_request(request_json, store_ap_json): # Announce is new content and votes that happened on a remote server. if request_json['type'] == 'Announce': - if isinstance(request_json['object'], str): # Mastodon, PeerTube, A.gup.pe - if request_json['object'].startswith('https://' + current_app.config['SERVER_NAME']): - log_incoming_ap(id, APLOG_DUPLICATE, APLOG_IGNORED, request_json if store_ap_json else None, 'Activity about local content which is already present') - return - post = resolve_remote_post(request_json['object'], community.id, announce_actor=community.ap_profile_id, store_ap_json=store_ap_json) - if post: - log_incoming_ap(id, APLOG_ANNOUNCE, APLOG_SUCCESS, request_json) - else: - log_incoming_ap(id, APLOG_ANNOUNCE, APLOG_FAILURE, request_json, 'Could not resolve post') - return - - user_ap_id = request_json['object']['actor'] - user = find_actor_or_create(user_ap_id) - if not user or not isinstance(user, User): - log_incoming_ap(id, APLOG_ANNOUNCE, APLOG_FAILURE, request_json if store_ap_json else None, 'Blocked or unfound user for Announce object actor ' + user_ap_id) - return - - user.last_seen = site.last_active = utcnow() - user.instance.last_seen = utcnow() - user.instance.dormant = False - user.instance.gone_forever = False - user.instance.failures = 0 - db.session.commit() + # should be able to remove the rest of this, and process the activities above. + # Then for any activities that are both sent direct and Announced, one can be dropped when shared_inbox() checks for dupes if request_json['object']['type'] == 'Create' or request_json['object']['type'] == 'Update': object_type = request_json['object']['object']['type'] @@ -1086,9 +1099,9 @@ def process_inbox_request(request_json, store_ap_json): log_incoming_ap(id, APLOG_DELETE, APLOG_FAILURE, request_json if store_ap_json else None, 'Delete: cannot find ' + ap_id) return - if request_json['object']['type'] == 'Like' or request_json['object']['type'] == 'EmojiReact': # Announced Upvote - process_upvote(user, store_ap_json, request_json) - return + #if request_json['object']['type'] == 'Like' or request_json['object']['type'] == 'EmojiReact': # Announced Upvote + # process_upvote(user, store_ap_json, request_json) + # return if request_json['object']['type'] == 'Dislike': # Announced Downvote if site.enable_downvotes is False: @@ -1623,7 +1636,7 @@ def process_new_content(user, community, store_ap_json, request_json, announced= return -def process_upvote(user, store_ap_json, request_json, announced=True): +def process_upvote(user, store_ap_json, request_json, announced): id = request_json['id'] ap_id = request_json['object'] if not announced else request_json['object']['object'] if isinstance(ap_id, dict) and 'id' in ap_id: From 172f8611415e18d9dcf2780f844aa0ae0829b164 Mon Sep 17 00:00:00 2001 From: freamon Date: Tue, 7 Jan 2025 14:14:50 +0000 Subject: [PATCH 12/16] Streamline ap routes (part 02: dislikes) --- app/activitypub/routes.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/app/activitypub/routes.py b/app/activitypub/routes.py index fd19eecb..cf694e00 100644 --- a/app/activitypub/routes.py +++ b/app/activitypub/routes.py @@ -860,11 +860,11 @@ def process_inbox_request(request_json, store_ap_json): process_upvote(user, store_ap_json, request_json, announced) return - if request_json['type'] == 'Dislike': # Downvote + if core_activity['type'] == 'Dislike': # Downvote if site.enable_downvotes is False: log_incoming_ap(id, APLOG_DISLIKE, APLOG_IGNORED, request_json if store_ap_json else None, 'Dislike ignored because of allow_dislike setting') return - process_downvote(user, store_ap_json, request_json, announced=False) + process_downvote(user, store_ap_json, request_json, announced) return if request_json['type'] == 'Flag': # Reported content @@ -1103,12 +1103,12 @@ def process_inbox_request(request_json, store_ap_json): # process_upvote(user, store_ap_json, request_json) # return - if request_json['object']['type'] == 'Dislike': # Announced Downvote - if site.enable_downvotes is False: - log_incoming_ap(id, APLOG_DISLIKE, APLOG_IGNORED, request_json if store_ap_json else None, 'Dislike ignored because of allow_dislike setting') - return - process_downvote(user, store_ap_json, request_json) - return + #if request_json['object']['type'] == 'Dislike': # Announced Downvote + # if site.enable_downvotes is False: + # log_incoming_ap(id, APLOG_DISLIKE, APLOG_IGNORED, request_json if store_ap_json else None, 'Dislike ignored because of allow_dislike setting') + # return + # process_downvote(user, store_ap_json, request_json) + # return if request_json['object']['type'] == 'Flag': # Announce of reported content reported = find_reported_object(request_json['object']['object']) @@ -1655,7 +1655,7 @@ def process_upvote(user, store_ap_json, request_json, announced): log_incoming_ap(id, APLOG_LIKE, APLOG_IGNORED, request_json if store_ap_json else None, 'Cannot upvote this') -def process_downvote(user, store_ap_json, request_json, announced=True): +def process_downvote(user, store_ap_json, request_json, announced): id = request_json['id'] ap_id = request_json['object'] if not announced else request_json['object']['object'] if isinstance(ap_id, dict) and 'id' in ap_id: From 88ac73f979ab2275f2397862476e244d3cf116fc Mon Sep 17 00:00:00 2001 From: freamon Date: Tue, 7 Jan 2025 15:12:28 +0000 Subject: [PATCH 13/16] Streamline ap routes (part 03: flags) --- app/activitypub/routes.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/app/activitypub/routes.py b/app/activitypub/routes.py index cf694e00..261e7df4 100644 --- a/app/activitypub/routes.py +++ b/app/activitypub/routes.py @@ -867,10 +867,10 @@ def process_inbox_request(request_json, store_ap_json): process_downvote(user, store_ap_json, request_json, announced) return - if request_json['type'] == 'Flag': # Reported content - reported = find_reported_object(request_json['object']) + if core_activity['type'] == 'Flag': # Reported content + reported = find_reported_object(core_activity['object']) if reported: - process_report(user, reported, request_json) + process_report(user, reported, core_activity) log_incoming_ap(id, APLOG_REPORT, APLOG_SUCCESS, request_json if store_ap_json else None) announce_activity_to_followers(reported.community, user, request_json) else: @@ -1110,14 +1110,14 @@ def process_inbox_request(request_json, store_ap_json): # process_downvote(user, store_ap_json, request_json) # return - if request_json['object']['type'] == 'Flag': # Announce of reported content - reported = find_reported_object(request_json['object']['object']) - if reported: - process_report(user, reported, request_json['object']) - log_incoming_ap(id, APLOG_REPORT, APLOG_SUCCESS, request_json if store_ap_json else None) - else: - log_incoming_ap(id, APLOG_REPORT, APLOG_IGNORED, request_json if store_ap_json else None, 'Report ignored due to missing content') - return + #if request_json['object']['type'] == 'Flag': # Announce of reported content + # reported = find_reported_object(request_json['object']['object']) + # if reported: + # process_report(user, reported, request_json['object']) + # log_incoming_ap(id, APLOG_REPORT, APLOG_SUCCESS, request_json if store_ap_json else None) + # else: + # log_incoming_ap(id, APLOG_REPORT, APLOG_IGNORED, request_json if store_ap_json else None, 'Report ignored due to missing content') + # return if request_json['object']['type'] == 'Lock': # Announce of post lock mod = user From 099f073f019d3771a1d9767e0ff940211f11786b Mon Sep 17 00:00:00 2001 From: freamon Date: Tue, 7 Jan 2025 15:27:28 +0000 Subject: [PATCH 14/16] Streamline ap routes (part 04: locks) --- app/activitypub/routes.py | 55 ++++++++++++++++++++++++++------------- 1 file changed, 37 insertions(+), 18 deletions(-) diff --git a/app/activitypub/routes.py b/app/activitypub/routes.py index 261e7df4..90db2e9c 100644 --- a/app/activitypub/routes.py +++ b/app/activitypub/routes.py @@ -877,6 +877,25 @@ def process_inbox_request(request_json, store_ap_json): log_incoming_ap(id, APLOG_REPORT, APLOG_IGNORED, request_json if store_ap_json else None, 'Report ignored due to missing content') return + if core_activity['type'] == 'Lock': # Post lock + mod = user + post_id = core_activity['object'] + post = Post.query.filter_by(ap_id=post_id).first() + reason = core_activity['summary'] if 'summary' in core_activity else '' + if post: + if post.community.is_moderator(mod) or post.community.is_instance_admin(mod): + post.comments_enabled = False + db.session.commit() + add_to_modlog_activitypub('lock_post', mod, community_id=post.community.id, + link_text=shorten_string(post.title), link=f'post/{post.id}', + reason=reason) + log_incoming_ap(id, APLOG_LOCK, APLOG_SUCCESS, request_json if store_ap_json else None) + else: + log_incoming_ap(id, APLOG_LOCK, APLOG_FAILURE, request_json if store_ap_json else None, 'Lock: Does not have permission') + else: + log_incoming_ap(id, APLOG_LOCK, APLOG_FAILURE, request_json if store_ap_json else None, 'Lock: post not found') + return + if request_json['type'] == 'Add': # remote site is adding a local user as a moderator, and is sending directly rather than announcing (happens if not subscribed) mod = user community = find_community(request_json) @@ -1119,24 +1138,24 @@ def process_inbox_request(request_json, store_ap_json): # log_incoming_ap(id, APLOG_REPORT, APLOG_IGNORED, request_json if store_ap_json else None, 'Report ignored due to missing content') # return - if request_json['object']['type'] == 'Lock': # Announce of post lock - mod = user - post_id = request_json['object']['object'] - post = Post.query.filter_by(ap_id=post_id).first() - reason = request_json['object']['summary'] if 'summary' in request_json['object'] else '' - if post: - if post.community.is_moderator(mod) or post.community.is_instance_admin(mod): - post.comments_enabled = False - db.session.commit() - add_to_modlog_activitypub('lock_post', mod, community_id=post.community.id, - link_text=shorten_string(post.title), link=f'post/{post.id}', - reason=reason) - log_incoming_ap(id, APLOG_LOCK, APLOG_SUCCESS, request_json if store_ap_json else None) - else: - log_incoming_ap(id, APLOG_LOCK, APLOG_FAILURE, request_json if store_ap_json else None, 'Lock: Does not have permission') - else: - log_incoming_ap(id, APLOG_LOCK, APLOG_FAILURE, request_json if store_ap_json else None, 'Lock: post not found') - return + #if request_json['object']['type'] == 'Lock': # Announce of post lock + # mod = user + # post_id = request_json['object']['object'] + # post = Post.query.filter_by(ap_id=post_id).first() + # reason = request_json['object']['summary'] if 'summary' in request_json['object'] else '' + # if post: + # if post.community.is_moderator(mod) or post.community.is_instance_admin(mod): + # post.comments_enabled = False + # db.session.commit() + # add_to_modlog_activitypub('lock_post', mod, community_id=post.community.id, + # link_text=shorten_string(post.title), link=f'post/{post.id}', + # reason=reason) + # log_incoming_ap(id, APLOG_LOCK, APLOG_SUCCESS, request_json if store_ap_json else None) + # else: + # log_incoming_ap(id, APLOG_LOCK, APLOG_FAILURE, request_json if store_ap_json else None, 'Lock: Does not have permission') + # else: + # log_incoming_ap(id, APLOG_LOCK, APLOG_FAILURE, request_json if store_ap_json else None, 'Lock: post not found') + # return if request_json['object']['type'] == 'Add': # Announce of adding mods or stickying a post target = request_json['object']['target'] From 3a9ffa7c53bdc01c9300fffd39bf532b6c2b39af Mon Sep 17 00:00:00 2001 From: freamon Date: Tue, 7 Jan 2025 16:23:29 +0000 Subject: [PATCH 15/16] Streamline ap routes (part 05: add mods or sticky) --- app/activitypub/routes.py | 85 ++++++++++++++++++++++----------------- 1 file changed, 47 insertions(+), 38 deletions(-) diff --git a/app/activitypub/routes.py b/app/activitypub/routes.py index 90db2e9c..d52228c7 100644 --- a/app/activitypub/routes.py +++ b/app/activitypub/routes.py @@ -896,18 +896,29 @@ def process_inbox_request(request_json, store_ap_json): log_incoming_ap(id, APLOG_LOCK, APLOG_FAILURE, request_json if store_ap_json else None, 'Lock: post not found') return - if request_json['type'] == 'Add': # remote site is adding a local user as a moderator, and is sending directly rather than announcing (happens if not subscribed) + if core_activity['type'] == 'Add': # Add mods, or sticky a post mod = user - community = find_community(request_json) + if not announced: + community = find_community(core_activity) if community: if not community.is_moderator(mod) and not community.is_instance_admin(mod): log_incoming_ap(id, APLOG_ADD, APLOG_FAILURE, request_json if store_ap_json else None, 'Does not have permission') return - target = request_json['target'] + target = core_activity['target'] + featured_url = community.ap_featured_url moderators_url = community.ap_moderators_url + if target == featured_url: + post = Post.query.filter_by(ap_id=core_activity['object']).first() + if post: + post.sticky = True + db.session.commit() + log_incoming_ap(id, APLOG_ADD, APLOG_SUCCESS, request_json if store_ap_json else None) + else: + log_incoming_ap(id, APLOG_ADD, APLOG_FAILURE, request_json if store_ap_json else None, 'Cannot find: ' + core_activity['object']) + return if target == moderators_url: - new_mod = find_actor_or_create(request_json['object'], create_if_not_found=False) - if new_mod and new_mod.is_local(): + new_mod = find_actor_or_create(core_activity['object']) + if new_mod: existing_membership = CommunityMember.query.filter_by(community_id=community.id, user_id=new_mod.id).first() if existing_membership: existing_membership.is_moderator = True @@ -917,11 +928,9 @@ def process_inbox_request(request_json, store_ap_json): db.session.commit() log_incoming_ap(id, APLOG_ADD, APLOG_SUCCESS, request_json if store_ap_json else None) else: - log_incoming_ap(id, APLOG_ADD, APLOG_FAILURE, request_json if store_ap_json else None, 'Cannot find: ' + request_json['object']) + log_incoming_ap(id, APLOG_ADD, APLOG_FAILURE, request_json if store_ap_json else None, 'Cannot find: ' + core_activity['object']) return - else: - # Lemmy might not send anything directly to sticky a post if no-one is subscribed (could not get it to generate the activity) - log_incoming_ap(id, APLOG_ADD, APLOG_FAILURE, request_json if store_ap_json else None, 'Unknown target for Add') + log_incoming_ap(id, APLOG_ADD, APLOG_FAILURE, request_json if store_ap_json else None, 'Unknown target for Add') else: log_incoming_ap(id, APLOG_ADD, APLOG_FAILURE, request_json if store_ap_json else None, 'Add: cannot find community') return @@ -1157,35 +1166,35 @@ def process_inbox_request(request_json, store_ap_json): # log_incoming_ap(id, APLOG_LOCK, APLOG_FAILURE, request_json if store_ap_json else None, 'Lock: post not found') # return - if request_json['object']['type'] == 'Add': # Announce of adding mods or stickying a post - target = request_json['object']['target'] - featured_url = community.ap_featured_url - moderators_url = community.ap_moderators_url - if target == featured_url: - post = Post.query.filter_by(ap_id=request_json['object']['object']).first() - if post: - post.sticky = True - db.session.commit() - log_incoming_ap(id, APLOG_ADD, APLOG_SUCCESS, request_json if store_ap_json else None) - else: - log_incoming_ap(id, APLOG_ADD, APLOG_FAILURE, request_json if store_ap_json else None, 'Cannot find: ' + request_json['object']['object']) - return - if target == moderators_url: - user = find_actor_or_create(request_json['object']['object']) - if user: - existing_membership = CommunityMember.query.filter_by(community_id=community.id, user_id=user.id).first() - if existing_membership: - existing_membership.is_moderator = True - else: - new_membership = CommunityMember(community_id=community.id, user_id=user.id, is_moderator=True) - db.session.add(new_membership) - db.session.commit() - log_incoming_ap(id, APLOG_ADD, APLOG_SUCCESS, request_json if store_ap_json else None) - else: - log_incoming_ap(id, APLOG_ADD, APLOG_FAILURE, request_json if store_ap_json else None, 'Cannot find: ' + request_json['object']['object']) - return - log_incoming_ap(id, APLOG_ADD, APLOG_FAILURE, request_json if store_ap_json else None, 'Unknown target for Add') - return + #if request_json['object']['type'] == 'Add': # Announce of adding mods or stickying a post + # target = request_json['object']['target'] + # featured_url = community.ap_featured_url + # moderators_url = community.ap_moderators_url + # if target == featured_url: + # post = Post.query.filter_by(ap_id=request_json['object']['object']).first() + # if post: + # post.sticky = True + # db.session.commit() + # log_incoming_ap(id, APLOG_ADD, APLOG_SUCCESS, request_json if store_ap_json else None) + # else: + # log_incoming_ap(id, APLOG_ADD, APLOG_FAILURE, request_json if store_ap_json else None, 'Cannot find: ' + request_json['object']['object']) + # return + # if target == moderators_url: + # user = find_actor_or_create(request_json['object']['object']) + # if user: + # existing_membership = CommunityMember.query.filter_by(community_id=community.id, user_id=user.id).first() + # if existing_membership: + # existing_membership.is_moderator = True + # else: + # new_membership = CommunityMember(community_id=community.id, user_id=user.id, is_moderator=True) + # db.session.add(new_membership) + # db.session.commit() + # log_incoming_ap(id, APLOG_ADD, APLOG_SUCCESS, request_json if store_ap_json else None) + # else: + # log_incoming_ap(id, APLOG_ADD, APLOG_FAILURE, request_json if store_ap_json else None, 'Cannot find: ' + request_json['object']['object']) + # return + # log_incoming_ap(id, APLOG_ADD, APLOG_FAILURE, request_json if store_ap_json else None, 'Unknown target for Add') + # return if request_json['object']['type'] == 'Remove': # Announce of removing mods or unstickying a post target = request_json['object']['target'] From 0f0202cb559786b76e1c5cc8864d1e5e5effa05a Mon Sep 17 00:00:00 2001 From: freamon Date: Tue, 7 Jan 2025 16:26:28 +0000 Subject: [PATCH 16/16] Streamline ap routes (part 06: remove mods or sticky) --- app/activitypub/routes.py | 79 ++++++++++++++++++++++----------------- 1 file changed, 44 insertions(+), 35 deletions(-) diff --git a/app/activitypub/routes.py b/app/activitypub/routes.py index d52228c7..41eccf7d 100644 --- a/app/activitypub/routes.py +++ b/app/activitypub/routes.py @@ -935,29 +935,38 @@ def process_inbox_request(request_json, store_ap_json): log_incoming_ap(id, APLOG_ADD, APLOG_FAILURE, request_json if store_ap_json else None, 'Add: cannot find community') return - if request_json['type'] == 'Remove': # remote site is removing a local user as a moderator, and is sending directly rather than announcing (happens if not subscribed) + if core_activity['type'] == 'Remove': # Remove mods, or unsticky a post mod = user - community = find_community(request_json) + if not announced: + community = find_community(core_activity) if community: if not community.is_moderator(mod) and not community.is_instance_admin(mod): log_incoming_ap(id, APLOG_ADD, APLOG_FAILURE, request_json if store_ap_json else None, 'Does not have permission') return - target = request_json['target'] + target = core_activity['target'] + featured_url = community.ap_featured_url moderators_url = community.ap_moderators_url + if target == featured_url: + post = Post.query.filter_by(ap_id=core_activity['object']).first() + if post: + post.sticky = False + db.session.commit() + log_incoming_ap(id, APLOG_REMOVE, APLOG_SUCCESS, request_json if store_ap_json else None) + else: + log_incoming_ap(id, APLOG_REMOVE, APLOG_FAILURE, request_json if store_ap_json else None, 'Cannot find: ' + core_activity['object']) + return if target == moderators_url: - old_mod = find_actor_or_create(request_json['object'], create_if_not_found=False) - if old_mod and old_mod.is_local(): + old_mod = find_actor_or_create(core_activity['object']) + if old_mod: existing_membership = CommunityMember.query.filter_by(community_id=community.id, user_id=old_mod.id).first() if existing_membership: existing_membership.is_moderator = False db.session.commit() log_incoming_ap(id, APLOG_REMOVE, APLOG_SUCCESS, request_json if store_ap_json else None) else: - log_incoming_ap(id, APLOG_ADD, APLOG_FAILURE, request_json if store_ap_json else None, 'Cannot find: ' + request_json['object']) + log_incoming_ap(id, APLOG_ADD, APLOG_FAILURE, request_json if store_ap_json else None, 'Cannot find: ' + core_activity['object']) return - else: - # Lemmy might not send anything directly to unsticky a post if no-one is subscribed (could not get it to generate the activity) - log_incoming_ap(id, APLOG_ADD, APLOG_FAILURE, request_json if store_ap_json else None, 'Unknown target for Remove') + log_incoming_ap(id, APLOG_ADD, APLOG_FAILURE, request_json if store_ap_json else None, 'Unknown target for Remove') else: log_incoming_ap(id, APLOG_ADD, APLOG_FAILURE, request_json if store_ap_json else None, 'Remove: cannot find community') return @@ -1196,32 +1205,32 @@ def process_inbox_request(request_json, store_ap_json): # log_incoming_ap(id, APLOG_ADD, APLOG_FAILURE, request_json if store_ap_json else None, 'Unknown target for Add') # return - if request_json['object']['type'] == 'Remove': # Announce of removing mods or unstickying a post - target = request_json['object']['target'] - featured_url = community.ap_featured_url - moderators_url = community.ap_moderators_url - if target == featured_url: - post = Post.query.filter_by(ap_id=request_json['object']['object']).first() - if post: - post.sticky = False - db.session.commit() - log_incoming_ap(id, APLOG_REMOVE, APLOG_SUCCESS, request_json if store_ap_json else None) - else: - log_incoming_ap(id, APLOG_REMOVE, APLOG_FAILURE, request_json if store_ap_json else None, 'Cannot find: ' + target) - return - if target == moderators_url: - user = find_actor_or_create(request_json['object']['object'], create_if_not_found=False) - if user: - existing_membership = CommunityMember.query.filter_by(community_id=community.id, user_id=user.id).first() - if existing_membership: - existing_membership.is_moderator = False - db.session.commit() - log_incoming_ap(id, APLOG_REMOVE, APLOG_SUCCESS, request_json if store_ap_json else None) - else: - log_incoming_ap(id, APLOG_REMOVE, APLOG_FAILURE, request_json if store_ap_json else None, 'Cannot find: ' + request_json['object']['object']) - return - log_incoming_ap(id, APLOG_REMOVE, APLOG_FAILURE, request_json if store_ap_json else None, 'Unknown target for Remove') - return + #if request_json['object']['type'] == 'Remove': # Announce of removing mods or unstickying a post + # target = request_json['object']['target'] + # featured_url = community.ap_featured_url + # moderators_url = community.ap_moderators_url + # if target == featured_url: + # post = Post.query.filter_by(ap_id=request_json['object']['object']).first() + # if post: + # post.sticky = False + # db.session.commit() + # log_incoming_ap(id, APLOG_REMOVE, APLOG_SUCCESS, request_json if store_ap_json else None) + # else: + # log_incoming_ap(id, APLOG_REMOVE, APLOG_FAILURE, request_json if store_ap_json else None, 'Cannot find: ' + target) + # return + # if target == moderators_url: + # user = find_actor_or_create(request_json['object']['object'], create_if_not_found=False) + # if user: + # existing_membership = CommunityMember.query.filter_by(community_id=community.id, user_id=user.id).first() + # if existing_membership: + # existing_membership.is_moderator = False + # db.session.commit() + # log_incoming_ap(id, APLOG_REMOVE, APLOG_SUCCESS, request_json if store_ap_json else None) + # else: + # log_incoming_ap(id, APLOG_REMOVE, APLOG_FAILURE, request_json if store_ap_json else None, 'Cannot find: ' + request_json['object']['object']) + # return + # log_incoming_ap(id, APLOG_REMOVE, APLOG_FAILURE, request_json if store_ap_json else None, 'Unknown target for Remove') + # return if request_json['object']['type'] == 'Block': # Announce of user ban. Mod is banning a user from a community, blocker = user # or an admin is banning a user from all the site's communities as part of a site ban