diff --git a/blueprints/api.py b/blueprints/api.py index cb71d39..aa7d8d3 100644 --- a/blueprints/api.py +++ b/blueprints/api.py @@ -77,6 +77,7 @@ def _api_required() -> None: # Will raise a BadSignature on bad auth payload = JWT.loads(token) + flask.g.jwt_payload = payload app.logger.info(f"api call by {payload}") @@ -438,13 +439,50 @@ def api_remove_from_list() -> _Response: return _user_api_response() -@blueprint.route("/new_note", methods=["POST"]) +@blueprint.route("/new_note", methods=["POST"]) # noqa: C901 too complex @api_required def api_new_note() -> _Response: - source = _user_api_arg("content") + source = None + summary = None + + # Basic Micropub (https://www.w3.org/TR/micropub/) "create" support + is_micropub = False + # First, check if the Micropub specific fields are present + if ( + _user_api_arg("h", default=None) == "entry" + or _user_api_arg("type", default=[None])[0] == "h-entry" + ): + is_micropub = True + # Ensure the "create" scope is set + if "jwt_payload" not in flask.g or "create" not in flask.g.jwt_payload["scope"]: + abort(403) + + # Handle JSON microformats2 data + if _user_api_arg("type", default=None): + try: + source = request.json["properties"]["content"][0] + except (ValueError, KeyError): + pass + try: + summary = request.json["properties"]["name"][0] + except (ValueError, KeyError): + pass + + # Try to parse the name as summary if the payload is POSTed using form-data + if summary is None: + summary = _user_api_arg("name", default=None) + + # This step will also parse content from Micropub request + if source is None: + source = _user_api_arg("content", default=None) + if not source: raise ValueError("missing content") + if summary is None: + summary = _user_api_arg("summary", default="") + + # All the following fields are specific to the API (i.e. not Micropub related) _reply, reply = None, None try: _reply = _user_api_arg("reply") @@ -490,7 +528,7 @@ def api_new_note() -> _Response: attributedTo=MY_PERSON.id, cc=list(set(cc)), to=list(set(to)), - summary=_user_api_arg("summary", default=""), + summary=summary, content=content, tag=tags, source={"mediaType": "text/markdown", "content": source}, @@ -498,27 +536,37 @@ def api_new_note() -> _Response: context=context, ) - if "file" in request.files and request.files["file"].filename: - file = request.files["file"] - rfilename = secure_filename(file.filename) - with BytesIO() as buf: - file.save(buf) - oid = MEDIA_CACHE.save_upload(buf, rfilename) - mtype = mimetypes.guess_type(rfilename)[0] + if request.files: + for f in request.files.keys(): + if not request.files[f].filename: + continue - raw_note["attachment"] = [ - { - "mediaType": mtype, - "name": _user_api_arg("file_description", default=rfilename), - "type": "Document", - "url": f"{BASE_URL}/uploads/{oid}/{rfilename}", - } - ] + file = request.files[f] + rfilename = secure_filename(file.filename) + with BytesIO() as buf: + file.save(buf) + oid = MEDIA_CACHE.save_upload(buf, rfilename) + mtype = mimetypes.guess_type(rfilename)[0] + + raw_note["attachment"] = [ + { + "mediaType": mtype, + "name": _user_api_arg("file_description", default=rfilename), + "type": "Document", + "url": f"{BASE_URL}/uploads/{oid}/{rfilename}", + } + ] note = ap.Note(**raw_note) create = note.build_create() create_id = post_to_outbox(create) + # Return a 201 with the note URL in the Location header if this was a Micropub request + if is_micropub: + resp = flask.Response("", headers={"Location": create_id}) + resp.status_code = 201 + return resp + return _user_api_response(activity=create_id) diff --git a/templates/layout.html b/templates/layout.html index 5293261..6216424 100644 --- a/templates/layout.html +++ b/templates/layout.html @@ -8,6 +8,7 @@ + {% if not request.args.get("older_than") and not request.args.get("previous_than") %}{% endif %} {% block links %}{% endblock %} {% if config.THEME_COLOR %}{% endif %}