mirror of
https://codeberg.org/rimu/pyfedi
synced 2025-01-23 19:36:56 -08:00
accept and follow with activitypub message logging
This commit is contained in:
parent
20dfc5a43b
commit
bfc4b243bf
5 changed files with 136 additions and 10 deletions
|
@ -1,3 +1,4 @@
|
||||||
|
import werkzeug.exceptions
|
||||||
from sqlalchemy import text
|
from sqlalchemy import text
|
||||||
|
|
||||||
from app import db
|
from app import db
|
||||||
|
@ -6,9 +7,10 @@ from flask import request, Response, render_template, current_app, abort, jsonif
|
||||||
|
|
||||||
from app.activitypub.signature import HttpSignature
|
from app.activitypub.signature import HttpSignature
|
||||||
from app.community.routes import show_community
|
from app.community.routes import show_community
|
||||||
from app.models import User, Community, CommunityJoinRequest, CommunityMember, CommunityBan
|
from app.models import User, Community, CommunityJoinRequest, CommunityMember, CommunityBan, ActivityPubLog
|
||||||
from app.activitypub.util import public_key, users_total, active_half_year, active_month, local_posts, local_comments, \
|
from app.activitypub.util import public_key, users_total, active_half_year, active_month, local_posts, local_comments, \
|
||||||
post_to_activity, find_actor_or_create
|
post_to_activity, find_actor_or_create
|
||||||
|
from app.utils import gibberish
|
||||||
|
|
||||||
INBOX = []
|
INBOX = []
|
||||||
|
|
||||||
|
@ -200,18 +202,77 @@ def community_profile(actor):
|
||||||
@bp.route('/inbox', methods=['GET', 'POST'])
|
@bp.route('/inbox', methods=['GET', 'POST'])
|
||||||
def shared_inbox():
|
def shared_inbox():
|
||||||
if request.method == 'POST':
|
if request.method == 'POST':
|
||||||
request_json = request.get_json()
|
# save all incoming data to aid in debugging and development
|
||||||
|
activity_log = ActivityPubLog(direction='in', activity_json=request.data, result='failure')
|
||||||
|
|
||||||
|
try:
|
||||||
|
request_json = request.get_json(force=True)
|
||||||
|
except werkzeug.exceptions.BadRequest as e:
|
||||||
|
activity_log.exception_message = 'Unable to parse json body: ' + e.description
|
||||||
|
db.session.add(activity_log)
|
||||||
|
db.session.commit()
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
if 'id' in request_json:
|
||||||
|
activity_log.activity_id = request_json['id']
|
||||||
|
|
||||||
actor = find_actor_or_create(request_json['actor'])
|
actor = find_actor_or_create(request_json['actor'])
|
||||||
if actor is not None:
|
if actor is not None:
|
||||||
if HttpSignature.verify_request(request, actor.public_key, skip_date=True):
|
if HttpSignature.verify_request(request, actor.public_key, skip_date=True):
|
||||||
if 'type' in request_json:
|
if 'type' in request_json:
|
||||||
|
activity_log.activity_type = request_json['type']
|
||||||
if request_json['type'] == 'Announce':
|
if request_json['type'] == 'Announce':
|
||||||
...
|
...
|
||||||
|
# remote user wants to follow one of our communities
|
||||||
elif request_json['type'] == 'Follow':
|
elif request_json['type'] == 'Follow':
|
||||||
# todo: send accept message if not banned
|
user_ap_id = request_json['actor']
|
||||||
banned = CommunityBan.query.filter_by(user_id=current_user.id,
|
community_ap_id = request_json['object']
|
||||||
community_id=community.id).first()
|
follow_id = request_json['id']
|
||||||
...
|
user = find_actor_or_create(user_ap_id)
|
||||||
|
community = find_actor_or_create(community_ap_id)
|
||||||
|
if user is not None and community is not None:
|
||||||
|
# check if user is banned from this community
|
||||||
|
banned = CommunityBan.query.filter_by(user_id=user.id,
|
||||||
|
community_id=community.id).first()
|
||||||
|
if banned is None:
|
||||||
|
if not user.subscribed(community):
|
||||||
|
member = CommunityMember(user_id=user.id, community_id=community.id)
|
||||||
|
db.session.add(member)
|
||||||
|
db.session.commit()
|
||||||
|
# send accept message to acknowledge the follow
|
||||||
|
accept = {
|
||||||
|
"@context": [
|
||||||
|
"https://www.w3.org/ns/activitystreams",
|
||||||
|
"https://w3id.org/security/v1",
|
||||||
|
],
|
||||||
|
"actor": community.ap_profile_id,
|
||||||
|
"to": [
|
||||||
|
user.ap_profile_id
|
||||||
|
],
|
||||||
|
"object": {
|
||||||
|
"actor": user.ap_profile_id,
|
||||||
|
"to": None,
|
||||||
|
"object": community.ap_profile_id,
|
||||||
|
"type": "Follow",
|
||||||
|
"id": follow_id
|
||||||
|
},
|
||||||
|
"type": "Accept",
|
||||||
|
"id": f"https://{current_app.config['SERVER_NAME']}/activities/accept/" + gibberish(32)
|
||||||
|
}
|
||||||
|
try:
|
||||||
|
HttpSignature.signed_request(user.ap_inbox_url, accept, community.private_key,
|
||||||
|
f"https://{current_app.config['SERVER_NAME']}/c/{community.name}#main-key")
|
||||||
|
except Exception as e:
|
||||||
|
accept_log = ActivityPubLog(direction='out', activity_json=json.dumps(accept),
|
||||||
|
result='failure', activity_id=accept['id'],
|
||||||
|
exception_message = 'could not send Accept' + str(e))
|
||||||
|
db.session.add(accept_log)
|
||||||
|
db.session.commit()
|
||||||
|
return
|
||||||
|
activity_log.result = 'success'
|
||||||
|
else:
|
||||||
|
activity_log.exception_message = 'user is banned from this community'
|
||||||
|
# remote server is accepting our previous follow request
|
||||||
elif request_json['type'] == 'Accept':
|
elif request_json['type'] == 'Accept':
|
||||||
if request_json['object']['type'] == 'Follow':
|
if request_json['object']['type'] == 'Follow':
|
||||||
community_ap_id = request_json['actor']
|
community_ap_id = request_json['actor']
|
||||||
|
@ -224,7 +285,15 @@ def shared_inbox():
|
||||||
member = CommunityMember(user_id=user.id, community_id=community.id)
|
member = CommunityMember(user_id=user.id, community_id=community.id)
|
||||||
db.session.add(member)
|
db.session.add(member)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
activity_log.result = 'success'
|
||||||
|
|
||||||
|
else:
|
||||||
|
activity_log.exception_message = 'Could not verify signature'
|
||||||
|
else:
|
||||||
|
activity_log.exception_message = 'Actor could not be found: ' + request_json['actor']
|
||||||
|
|
||||||
|
db.session.add(activity_log)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
|
||||||
@bp.route('/c/<actor>/outbox', methods=['GET'])
|
@bp.route('/c/<actor>/outbox', methods=['GET'])
|
||||||
|
|
|
@ -26,7 +26,7 @@ def add_local():
|
||||||
private_key, public_key = RsaKeys.generate_keypair()
|
private_key, public_key = RsaKeys.generate_keypair()
|
||||||
community = Community(title=form.community_name.data, name=form.url.data, description=form.description.data,
|
community = Community(title=form.community_name.data, name=form.url.data, description=form.description.data,
|
||||||
rules=form.rules.data, nsfw=form.nsfw.data, private_key=private_key,
|
rules=form.rules.data, nsfw=form.nsfw.data, private_key=private_key,
|
||||||
public_key=public_key,
|
public_key=public_key, ap_profile_id=current_app.config['SERVER_NAME'] + '/c/' + form.url.data,
|
||||||
subscriptions_count=1)
|
subscriptions_count=1)
|
||||||
db.session.add(community)
|
db.session.add(community)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
|
@ -310,6 +310,7 @@ class CommunityMember(db.Model):
|
||||||
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||||
|
|
||||||
|
|
||||||
|
# people banned from communities
|
||||||
class CommunityBan(db.Model):
|
class CommunityBan(db.Model):
|
||||||
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), primary_key=True)
|
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), primary_key=True)
|
||||||
community_id = db.Column(db.Integer, db.ForeignKey('community.id'), primary_key=True)
|
community_id = db.Column(db.Integer, db.ForeignKey('community.id'), primary_key=True)
|
||||||
|
@ -373,6 +374,18 @@ class UserFollowRequest(db.Model):
|
||||||
follow_id = db.Column(db.Integer, db.ForeignKey('user.id'))
|
follow_id = db.Column(db.Integer, db.ForeignKey('user.id'))
|
||||||
|
|
||||||
|
|
||||||
|
# save every activity to a log, to aid debugging
|
||||||
|
class ActivityPubLog(db.Model):
|
||||||
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
direction = db.Column(db.String(3)) # 'in' or 'out'
|
||||||
|
activity_id = db.Column(db.String(100), indexed=True)
|
||||||
|
activity_type = db.Column(db.String(50)) # e.g. 'Follow', 'Accept', 'Like', etc
|
||||||
|
activity_json = db.Column(db.Text) # the full json of the activity
|
||||||
|
result = db.Column(db.String(10)) # 'success' or 'failure'
|
||||||
|
exception_message = db.Column(db.Text)
|
||||||
|
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||||
|
|
||||||
|
|
||||||
@login.user_loader
|
@login.user_loader
|
||||||
def load_user(id):
|
def load_user(id):
|
||||||
return User.query.get(int(id))
|
return User.query.get(int(id))
|
||||||
|
|
13
app/utils.py
13
app/utils.py
|
@ -1,10 +1,8 @@
|
||||||
import functools
|
import functools
|
||||||
|
import random
|
||||||
import requests
|
import requests
|
||||||
import os
|
import os
|
||||||
|
|
||||||
from flask import current_app, json
|
from flask import current_app, json
|
||||||
|
|
||||||
from app import db
|
from app import db
|
||||||
from app.models import Settings
|
from app.models import Settings
|
||||||
|
|
||||||
|
@ -30,6 +28,7 @@ def get_request(uri, params=None, headers=None) -> requests.Response:
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
# saves an arbitrary object into a persistent key-value store. Possibly redis would be faster than using the DB
|
||||||
@functools.lru_cache(maxsize=100)
|
@functools.lru_cache(maxsize=100)
|
||||||
def get_setting(name: str, default=None):
|
def get_setting(name: str, default=None):
|
||||||
setting = Settings.query.filter_by(name=name).first()
|
setting = Settings.query.filter_by(name=name).first()
|
||||||
|
@ -39,6 +38,7 @@ def get_setting(name: str, default=None):
|
||||||
return json.loads(setting.value)
|
return json.loads(setting.value)
|
||||||
|
|
||||||
|
|
||||||
|
# retrieves arbitrary object from persistent key-value store
|
||||||
def set_setting(name: str, value):
|
def set_setting(name: str, value):
|
||||||
setting = Settings.query.filter_by(name=name).first()
|
setting = Settings.query.filter_by(name=name).first()
|
||||||
if setting is None:
|
if setting is None:
|
||||||
|
@ -54,3 +54,10 @@ def file_get_contents(filename):
|
||||||
with open(filename, 'r') as file:
|
with open(filename, 'r') as file:
|
||||||
contents = file.read()
|
contents = file.read()
|
||||||
return contents
|
return contents
|
||||||
|
|
||||||
|
|
||||||
|
random_chars = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'
|
||||||
|
|
||||||
|
|
||||||
|
def gibberish(length: int = 10) -> str:
|
||||||
|
return "".join([random.choice(random_chars) for x in range(length)])
|
||||||
|
|
37
migrations/versions/f032dbdfbd1d_activitypub_debug_log.py
Normal file
37
migrations/versions/f032dbdfbd1d_activitypub_debug_log.py
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
"""activitypub debug log
|
||||||
|
|
||||||
|
Revision ID: f032dbdfbd1d
|
||||||
|
Revises: cc98a471a1ad
|
||||||
|
Create Date: 2023-09-09 20:06:28.257769
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = 'f032dbdfbd1d'
|
||||||
|
down_revision = 'cc98a471a1ad'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.create_table('activity_pub_log',
|
||||||
|
sa.Column('id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('direction', sa.String(length=3), nullable=True),
|
||||||
|
sa.Column('activity_type', sa.String(length=50), nullable=True),
|
||||||
|
sa.Column('activity_json', sa.Text(), nullable=True),
|
||||||
|
sa.Column('result', sa.String(length=10), nullable=True),
|
||||||
|
sa.Column('exception_message', sa.Text(), nullable=True),
|
||||||
|
sa.Column('created_at', sa.DateTime(), nullable=True),
|
||||||
|
sa.PrimaryKeyConstraint('id')
|
||||||
|
)
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.drop_table('activity_pub_log')
|
||||||
|
# ### end Alembic commands ###
|
Loading…
Reference in a new issue