From c9beb0c0dafeec22c3d0d39f374968ecd3547fa4 Mon Sep 17 00:00:00 2001 From: rimu <3310831+rimu@users.noreply.github.com> Date: Thu, 16 Nov 2023 22:31:14 +1300 Subject: [PATCH] federation: handle remote subscriptions and unsubscriptions --- app/activitypub/routes.py | 37 ++++++++++++++++++++++++++---------- app/activitypub/signature.py | 3 ++- app/activitypub/util.py | 6 ++++-- app/community/forms.py | 12 ++++++++++++ app/main/routes.py | 1 + app/models.py | 1 - app/static/js/scripts.js | 2 +- app/utils.py | 6 +++++- 8 files changed, 52 insertions(+), 16 deletions(-) diff --git a/app/activitypub/routes.py b/app/activitypub/routes.py index 2c91ad96..56e426d9 100644 --- a/app/activitypub/routes.py +++ b/app/activitypub/routes.py @@ -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//outbox', methods=['GET']) diff --git a/app/activitypub/signature.py b/app/activitypub/signature.py index ae3e1146..d40b2461 100644 --- a/app/activitypub/signature.py +++ b/app/activitypub/signature.py @@ -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)"] diff --git a/app/activitypub/util.py b/app/activitypub/util.py index 2a855e7b..536b4435 100644 --- a/app/activitypub/util.py +++ b/app/activitypub/util.py @@ -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'], diff --git a/app/community/forms.py b/app/community/forms.py index 845806a2..2661ec6e 100644 --- a/app/community/forms.py +++ b/app/community/forms.py @@ -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()]) diff --git a/app/main/routes.py b/app/main/routes.py index e2e55ebc..ca15e774 100644 --- a/app/main/routes.py +++ b/app/main/routes.py @@ -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') diff --git a/app/models.py b/app/models.py index d336ea22..90f08155 100644 --- a/app/models.py +++ b/app/models.py @@ -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) diff --git a/app/static/js/scripts.js b/app/static/js/scripts.js index a1116883..46028b44 100644 --- a/app/static/js/scripts.js +++ b/app/static/js/scripts.js @@ -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, '_'); } \ No newline at end of file diff --git a/app/utils.py b/app/utils.py index 79e82783..cbf51366 100644 --- a/app/utils.py +++ b/app/utils.py @@ -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}")