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 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.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, \
|
||||
post_to_activity, find_actor_or_create
|
||||
from app.utils import gibberish
|
||||
|
||||
INBOX = []
|
||||
|
||||
|
@ -200,18 +202,77 @@ def community_profile(actor):
|
|||
@bp.route('/inbox', methods=['GET', 'POST'])
|
||||
def shared_inbox():
|
||||
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'])
|
||||
if actor is not None:
|
||||
if HttpSignature.verify_request(request, actor.public_key, skip_date=True):
|
||||
if 'type' in request_json:
|
||||
activity_log.activity_type = request_json['type']
|
||||
if request_json['type'] == 'Announce':
|
||||
...
|
||||
# remote user wants to follow one of our communities
|
||||
elif request_json['type'] == 'Follow':
|
||||
# todo: send accept message if not banned
|
||||
banned = CommunityBan.query.filter_by(user_id=current_user.id,
|
||||
user_ap_id = request_json['actor']
|
||||
community_ap_id = request_json['object']
|
||||
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':
|
||||
if request_json['object']['type'] == 'Follow':
|
||||
community_ap_id = request_json['actor']
|
||||
|
@ -224,7 +285,15 @@ def shared_inbox():
|
|||
member = CommunityMember(user_id=user.id, community_id=community.id)
|
||||
db.session.add(member)
|
||||
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'])
|
||||
|
|
|
@ -26,7 +26,7 @@ def add_local():
|
|||
private_key, public_key = RsaKeys.generate_keypair()
|
||||
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,
|
||||
public_key=public_key,
|
||||
public_key=public_key, ap_profile_id=current_app.config['SERVER_NAME'] + '/c/' + form.url.data,
|
||||
subscriptions_count=1)
|
||||
db.session.add(community)
|
||||
db.session.commit()
|
||||
|
|
|
@ -310,6 +310,7 @@ class CommunityMember(db.Model):
|
|||
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||
|
||||
|
||||
# people banned from communities
|
||||
class CommunityBan(db.Model):
|
||||
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)
|
||||
|
@ -373,6 +374,18 @@ class UserFollowRequest(db.Model):
|
|||
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
|
||||
def load_user(id):
|
||||
return User.query.get(int(id))
|
||||
|
|
13
app/utils.py
13
app/utils.py
|
@ -1,10 +1,8 @@
|
|||
import functools
|
||||
|
||||
import random
|
||||
import requests
|
||||
import os
|
||||
|
||||
from flask import current_app, json
|
||||
|
||||
from app import db
|
||||
from app.models import Settings
|
||||
|
||||
|
@ -30,6 +28,7 @@ def get_request(uri, params=None, headers=None) -> requests.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)
|
||||
def get_setting(name: str, default=None):
|
||||
setting = Settings.query.filter_by(name=name).first()
|
||||
|
@ -39,6 +38,7 @@ def get_setting(name: str, default=None):
|
|||
return json.loads(setting.value)
|
||||
|
||||
|
||||
# retrieves arbitrary object from persistent key-value store
|
||||
def set_setting(name: str, value):
|
||||
setting = Settings.query.filter_by(name=name).first()
|
||||
if setting is None:
|
||||
|
@ -54,3 +54,10 @@ def file_get_contents(filename):
|
|||
with open(filename, 'r') as file:
|
||||
contents = file.read()
|
||||
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