federation: handle remote subscriptions and unsubscriptions

This commit is contained in:
rimu 2023-11-16 22:31:14 +13:00
parent c3d36cfb86
commit c9beb0c0da
8 changed files with 52 additions and 16 deletions

View file

@ -77,7 +77,7 @@ def nodeinfo2():
nodeinfo_data = {
"version": "2.0",
"software": {
"name": "pyfedi",
"name": "PieFed",
"version": "0.1"
},
"protocols": [
@ -151,18 +151,18 @@ def community_profile(actor):
# don't provide activitypub info for remote communities
if 'application/ld+json' in request.headers.get('Accept', ''):
abort(404)
community = Community.query.filter_by(ap_id=actor, banned=False).first()
community: Community = Community.query.filter_by(ap_id=actor, banned=False).first()
else:
community = Community.query.filter_by(name=actor, banned=False, ap_id=None).first()
community: Community = Community.query.filter_by(name=actor, banned=False, ap_id=None).first()
if community is not None:
if 'application/ld+json' in request.headers.get('Accept', ''):
server = current_app.config['SERVER_NAME']
actor_data = {"@context": default_context(),
"type": "Group",
"id": f"https://{server}/c/{actor}",
"name": actor.title,
"summary": actor.description,
"sensitive": True if actor.nsfw or actor.nsfl else False,
"name": community.title,
"summary": community.description,
"sensitive": True if community.nsfw or community.nsfl else False,
"preferredUsername": actor,
"inbox": f"https://{server}/c/{actor}/inbox",
"outbox": f"https://{server}/c/{actor}/outbox",
@ -170,7 +170,7 @@ def community_profile(actor):
"moderators": f"https://{server}/c/{actor}/moderators",
"featured": f"https://{server}/c/{actor}/featured",
"attributedTo": f"https://{server}/c/{actor}/moderators",
"postingRestrictedToMods": actor.restricted_to_mods,
"postingRestrictedToMods": community.restricted_to_mods,
"url": f"https://{server}/c/{actor}",
"publicKey": {
"id": f"https://{server}/c/{actor}#main-key",
@ -180,13 +180,13 @@ def community_profile(actor):
"endpoints": {
"sharedInbox": f"https://{server}/inbox"
},
"published": community.created.isoformat(),
"published": community.created_at.isoformat(),
"updated": community.last_active.isoformat(),
}
if community.avatar_id is not None:
if community.icon_id is not None:
actor_data["icon"] = {
"type": "Image",
"url": f"https://{server}/avatars/{community.avatar.file_path}"
"url": f"https://{server}/avatars/{community.icon.file_path}"
}
resp = jsonify(actor_data)
resp.content_type = 'application/activity+json'
@ -411,6 +411,22 @@ def shared_inbox():
community.subscriptions_count += 1
db.session.commit()
activity_log.result = 'success'
elif request_json['type'] == 'Undo':
if request_json['object']['type'] == 'Follow': # Unsubscribe from a community
community_ap_id = request_json['actor']
user_ap_id = request_json['object']['actor']
user = find_actor_or_create(user_ap_id)
community = find_actor_or_create(community_ap_id)
if user and community:
member = CommunityMember.query.filter_by(user_id=user.id, community_id=community.id).first()
db.session.delete(member)
db.session.commit()
activity_log.result = 'success'
elif request_json['object']['type'] == 'Like': # Undoing an upvote
...
elif request_json['object']['type'] == 'Dislike': # Undoing a downvote
...
else:
activity_log.exception_message = 'Instance banned'
else:
@ -422,6 +438,7 @@ def shared_inbox():
activity_log.result = 'failure'
db.session.add(activity_log)
db.session.commit()
return ''
@bp.route('/c/<actor>/outbox', methods=['GET'])

View file

@ -236,6 +236,7 @@ class HttpSignature:
headers_string,
public_key,
)
return True
@classmethod
def signed_request(
@ -298,7 +299,7 @@ class HttpSignature:
)
# Announce ourselves with an agent similar to Mastodon
headers["User-Agent"] = 'pyfedi'
headers["User-Agent"] = 'PieFed'
# Send the request with all those headers except the pseudo one
del headers["(request-target)"]

View file

@ -214,7 +214,7 @@ def find_actor_or_create(actor: str) -> Union[User, Community, None]:
return None
user = User.query.filter_by(
ap_profile_id=actor).first() # finds users formatted like https://kbin.social/u/tables
if user.banned:
if user and user.banned:
return None
if user is None:
user = Community.query.filter_by(ap_profile_id=actor).first()
@ -225,6 +225,7 @@ def find_actor_or_create(actor: str) -> Union[User, Community, None]:
params={'resource': f"acct:{address}@{server}"})
if webfinger_data.status_code == 200:
webfinger_json = webfinger_data.json()
webfinger_data.close()
for links in webfinger_json['links']:
if 'rel' in links and links['rel'] == 'self': # this contains the URL of the activitypub profile
type = links['type'] if 'type' in links else 'application/activity+json'
@ -233,11 +234,12 @@ def find_actor_or_create(actor: str) -> Union[User, Community, None]:
# to see the structure of the json contained in actor_data, do a GET to https://lemmy.world/c/technology with header Accept: application/activity+json
if actor_data.status_code == 200:
activity_json = actor_data.json()
actor_data.close()
if activity_json['type'] == 'Person':
user = User(user_name=activity_json['preferredUsername'],
email=f"{address}@{server}",
about=parse_summary(activity_json),
created_at=activity_json['published'],
created=activity_json['published'],
ap_id=f"{address}@{server}",
ap_public_url=activity_json['id'],
ap_profile_id=activity_json['id'],

View file

@ -14,6 +14,18 @@ class AddLocalCommunity(FlaskForm):
nsfw = BooleanField('18+ NSFW')
submit = SubmitField(_l('Create'))
def validate(self, extra_validators=None):
if not super().validate():
return False
if self.url.data.strip() == '':
self.url.errors.append(_('Url is required.'))
return False
else:
if '-' in self.url.data.strip():
self.url.errors.append(_('- cannot be in Url. Use _ instead?'))
return False
return True
class SearchRemoteCommunity(FlaskForm):
address = StringField(_l('Server address'), validators=[DataRequired()])

View file

@ -15,6 +15,7 @@ from app.models import Community, CommunityMember
@bp.route('/', methods=['GET', 'POST'])
@bp.route('/index', methods=['GET', 'POST'])
def index():
raise Exception('cowbell')
verification_warning()
return render_template('index.html')

View file

@ -297,7 +297,6 @@ class User(UserMixin, db.Model):
{'user_id': self.id})
class ActivityLog(db.Model):
id = db.Column(db.Integer, primary_key=True)
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), index=True)

View file

@ -177,5 +177,5 @@ function setupHideButtons() {
function titleToURL(title) {
// Convert the title to lowercase and replace spaces with hyphens
return title.toLowerCase().replace(/\s+/g, '-');
return title.toLowerCase().replace(/\s+/g, '_');
}

View file

@ -33,8 +33,12 @@ def getmtime(filename):
# do a GET request to a uri, return the result
def get_request(uri, params=None, headers=None) -> requests.Response:
if headers is None:
headers = {'User-Agent': 'PieFed/1.0'}
else:
headers.update({'User-Agent': 'PieFed/1.0'})
try:
response = requests.get(uri, params=params, headers=headers, timeout=1, allow_redirects=True)
response = requests.get(uri, params=params, headers=headers, timeout=5, allow_redirects=True)
except requests.exceptions.SSLError as invalid_cert:
# Not our problem if the other end doesn't have proper SSL
current_app.logger.info(f"{uri} {invalid_cert}")