Esempio n. 1
0
def authorize_follow():
    if request.method == "GET":
        return htmlify(
            render_template("authorize_remote_follow.html",
                            profile=request.args.get("profile")))

    csrf.protect()
    actor = get_actor_url(request.form.get("profile"))
    if not actor:
        abort(500)

    q = {
        "box": Box.OUTBOX.value,
        "type": ap.ActivityType.FOLLOW.value,
        "meta.undo": False,
        "activity.object": actor,
    }
    if DB.activities.count(q) > 0:
        return redirect("/following")

    follow = ap.Follow(actor=MY_PERSON.id,
                       object=actor,
                       to=[actor],
                       cc=[ap.AS_PUBLIC],
                       published=now())
    post_to_outbox(follow)

    return redirect("/following")
Esempio n. 2
0
def admin_update_actor() -> _Response:
    update = ap.Update(
        actor=MY_PERSON.id,
        object=MY_PERSON.to_dict(),
        to=[MY_PERSON.followers],
        cc=[ap.AS_PUBLIC],
        published=now(),
    )

    post_to_outbox(update)
    return "OK"
Esempio n. 3
0
def admin_update_actor() -> _Response:
    # FIXME(tsileo): make this a task, and keep track of our own actor_hash at startup
    update = ap.Update(
        actor=MY_PERSON.id,
        object=MY_PERSON.to_dict(),
        to=[MY_PERSON.followers],
        cc=[ap.AS_PUBLIC],
        published=now(),
    )

    post_to_outbox(update)
    return "OK"
Esempio n. 4
0
def api_follow() -> _Response:
    actor = _user_api_arg("actor")

    q = {
        "box": Box.OUTBOX.value,
        "type": ap.ActivityType.FOLLOW.value,
        "meta.undo": False,
        "activity.object": actor,
    }

    existing = DB.activities.find_one(q)
    if existing:
        return _user_api_response(activity=existing["activity"]["id"])

    follow = ap.Follow(
        actor=MY_PERSON.id,
        object=actor,
        to=[actor],
        cc=[ap.AS_PUBLIC],
        published=now(),
        context=new_context(),
    )
    follow_id = post_to_outbox(follow)

    return _user_api_response(activity=follow_id)
Esempio n. 5
0
def outbox():
    if request.method == "GET":
        if not is_api_request():
            abort(404)
        _log_sig()
        # TODO(tsileo): returns the whole outbox if authenticated and look at OCAP support
        q = {
            "box": Box.OUTBOX.value,
            "meta.deleted": False,
            "meta.undo": False,
            "meta.public": True,
            "type": {
                "$in":
                [ActivityType.CREATE.value, ActivityType.ANNOUNCE.value]
            },
        }
        return jsonify(**activitypub.build_ordered_collection(
            DB.activities,
            q=q,
            cursor=request.args.get("cursor"),
            map_func=lambda doc: activity_from_doc(doc, embed=True),
            col_name="outbox",
        ))

    # Handle POST request
    try:
        _api_required()
    except BadSignature:
        abort(401)

    data = request.get_json(force=True)
    activity = ap.parse_activity(data)
    activity_id = post_to_outbox(activity)

    return Response(status=201, headers={"Location": activity_id})
Esempio n. 6
0
def api_undo() -> _Response:
    oid = _user_api_arg("id")
    doc = DB.activities.find_one(
        {
            "box": Box.OUTBOX.value,
            "$or": [{"remote_id": activity_url(oid)}, {"remote_id": oid}],
        }
    )
    if not doc:
        raise ActivityNotFoundError(f"cannot found {oid}")

    obj = ap.parse_activity(doc.get("activity"))

    undo = ap.Undo(
        actor=MY_PERSON.id,
        context=new_context(obj),
        object=obj.to_dict(embed=True, embed_object_id_only=True),
        published=now(),
        to=obj.to,
        cc=obj.cc,
    )

    # FIXME(tsileo): detect already undo-ed and make this API call idempotent
    undo_id = post_to_outbox(undo)

    return _user_api_response(activity=undo_id)
Esempio n. 7
0
def _follow_process_inbox(activity: ap.Follow, new_meta: _NewMeta) -> None:
    _logger.info(f"process_inbox activity={activity!r}")
    # Reply to a Follow with an Accept
    actor_id = activity.get_actor().id
    accept = ap.Accept(
        actor=config.ID,
        object={
            "type": "Follow",
            "id": activity.id,
            "object": activity.get_object_id(),
            "actor": actor_id,
        },
        to=[actor_id],
        published=now(),
    )
    post_to_outbox(accept)
Esempio n. 8
0
def api_like() -> _Response:
    note = _user_api_get_note()

    to: List[str] = []
    cc: List[str] = []

    note_visibility = ap.get_visibility(note)

    if note_visibility == ap.Visibility.PUBLIC:
        to = [ap.AS_PUBLIC]
        cc = [ID + "/followers", note.get_actor().id]
    elif note_visibility == ap.Visibility.UNLISTED:
        to = [ID + "/followers", note.get_actor().id]
        cc = [ap.AS_PUBLIC]
    else:
        to = [note.get_actor().id]

    like = ap.Like(
        object=note.id,
        actor=MY_PERSON.id,
        to=to,
        cc=cc,
        published=now(),
        context=new_context(note),
    )

    like_id = post_to_outbox(like)

    return _user_api_response(activity=like_id)
Esempio n. 9
0
def api_ack_reply() -> _Response:
    reply_iri = _user_api_arg("reply_iri")
    obj = ap.fetch_remote_activity(reply_iri)
    if obj.has_type(ap.ActivityType.CREATE):
        obj = obj.get_object()
    # TODO(tsileo): tweak the adressing?
    update_one_activity(
        {
            **by_type(ap.ActivityType.CREATE),
            **by_object_id(obj.id)
        },
        {"$set": {
            "meta.reply_acked": True
        }},
    )
    read = ap.Read(
        actor=MY_PERSON.id,
        object=obj.id,
        to=[MY_PERSON.followers],
        cc=[ap.AS_PUBLIC, obj.get_actor().id],
        published=now(),
        context=new_context(obj),
    )

    read_id = post_to_outbox(read)
    return _user_api_response(activity=read_id)
Esempio n. 10
0
def api_new_question() -> _Response:
    source = _user_api_arg("content")
    if not source:
        raise ValueError("missing content")

    content, tags = parse_markdown(source)
    cc = [ID + "/followers"]

    for tag in tags:
        if tag["type"] == "Mention":
            cc.append(tag["href"])

    answers = []
    for i in range(4):
        a = _user_api_arg(f"answer{i}", default=None)
        if not a:
            break
        answers.append({
            "type": ap.ActivityType.NOTE.value,
            "name": a,
            "replies": {
                "type": ap.ActivityType.COLLECTION.value,
                "totalItems": 0
            },
        })

    open_for = int(_user_api_arg("open_for"))
    choices = {
        "endTime":
        ap.format_datetime(
            datetime.now(timezone.utc) + timedelta(minutes=open_for))
    }
    of = _user_api_arg("of")
    if of == "anyOf":
        choices["anyOf"] = answers
    else:
        choices["oneOf"] = answers

    raw_question = dict(
        attributedTo=MY_PERSON.id,
        cc=list(set(cc)),
        to=[ap.AS_PUBLIC],
        content=content,
        tag=tags,
        source={
            "mediaType": "text/markdown",
            "content": source
        },
        inReplyTo=None,
        **choices,
    )

    question = ap.Question(**raw_question)
    create = question.build_create()
    create_id = post_to_outbox(create)

    Tasks.update_question_outbox(create_id, open_for)

    return _user_api_response(activity=create_id)
Esempio n. 11
0
def task_send_actor_update() -> _Response:
    task = p.parse(flask.request)
    app.logger.info(f"task={task!r}")
    try:
        update = ap.Update(
            actor=MY_PERSON.id,
            object=MY_PERSON.to_dict(),
            to=[MY_PERSON.followers],
            cc=[ap.AS_PUBLIC],
            published=now(),
            context=new_context(),
        )

        post_to_outbox(update)
    except Exception as err:
        app.logger.exception(f"failed to send actor update")
        raise TaskError() from err

    return ""
Esempio n. 12
0
def task_update_question() -> _Response:
    """Sends an Update."""
    task = p.parse(flask.request)
    app.logger.info(f"task={task!r}")
    iri = task.payload
    try:
        app.logger.info(f"Updating question {iri}")
        cc = [config.ID + "/followers"]
        doc = DB.activities.find_one({
            "box": Box.OUTBOX.value,
            "remote_id": iri
        })
        _add_answers_to_question(doc)
        question = ap.Question(**doc["activity"]["object"])

        raw_update = dict(
            actor=question.id,
            object=question.to_dict(embed=True),
            attributedTo=MY_PERSON.id,
            cc=list(set(cc)),
            to=[ap.AS_PUBLIC],
        )
        raw_update["@context"] = config.DEFAULT_CTX

        update = ap.Update(**raw_update)
        print(update)
        print(update.to_dict())
        post_to_outbox(update)

    except HTTPError as err:
        app.logger.exception("request failed")
        if 400 >= err.response.status_code >= 499:
            app.logger.info("client error, no retry")
            return ""

        raise TaskError() from err
    except Exception as err:
        app.logger.exception("task failed")
        raise TaskError() from err

    return ""
Esempio n. 13
0
def api_delete() -> _Response:
    """API endpoint to delete a Note activity."""
    note = _user_api_get_note(from_outbox=True)

    # Create the delete, same audience as the Create object
    delete = ap.Delete(
        actor=ID,
        object=ap.Tombstone(id=note.id).to_dict(embed=True),
        to=note.to,
        cc=note.cc,
        published=now(),
    )

    delete_id = post_to_outbox(delete)

    return _user_api_response(activity=delete_id)
Esempio n. 14
0
def api_block() -> _Response:
    actor = _user_api_arg("actor")

    existing = DB.activities.find_one({
        "box": Box.OUTBOX.value,
        "type": ap.ActivityType.BLOCK.value,
        "activity.object": actor,
        "meta.undo": False,
    })
    if existing:
        return _user_api_response(activity=existing["activity"]["id"])

    block = ap.Block(actor=MY_PERSON.id, object=actor)
    block_id = post_to_outbox(block)

    return _user_api_response(activity=block_id)
Esempio n. 15
0
def api_boost() -> _Response:
    note = _user_api_get_note()

    # Ensures the note visibility allow us to build an Announce (in respect to the post visibility)
    if ap.get_visibility(note) not in [ap.Visibility.PUBLIC, ap.Visibility.UNLISTED]:
        abort(400)

    announce = ap.Announce(
        actor=MY_PERSON.id,
        object=note.id,
        to=[MY_PERSON.followers, note.attributedTo],
        cc=[ap.AS_PUBLIC],
        published=now(),
        context=new_context(note),
    )
    announce_id = post_to_outbox(announce)

    return _user_api_response(activity=announce_id)
Esempio n. 16
0
def api_vote() -> _Response:
    oid = _user_api_arg("id")
    app.logger.info(f"fetching {oid}")
    note = ap.parse_activity(ap.get_backend().fetch_iri(oid))
    choice = _user_api_arg("choice")

    raw_note = dict(
        attributedTo=MY_PERSON.id,
        cc=[],
        to=note.get_actor().id,
        name=choice,
        tag=[],
        inReplyTo=note.id,
    )
    raw_note["@context"] = config.DEFAULT_CTX

    note = ap.Note(**raw_note)
    create = note.build_create()
    create_id = post_to_outbox(create)

    return _user_api_response(activity=create_id)
Esempio n. 17
0
def outbox():
    if request.method == "GET":
        if not is_api_request():
            abort(404)
        _log_sig()
        # TODO(tsileo): returns the whole outbox if authenticated and look at OCAP support
        q = {
            **in_outbox(),
            "$or": [
                {
                    **by_type(ActivityType.CREATE),
                    **not_deleted(),
                    **by_visibility(ap.Visibility.PUBLIC),
                },
                {
                    **by_type(ActivityType.ANNOUNCE),
                    **not_undo()
                },
            ],
        }
        return activitypubify(**activitypub.build_ordered_collection(
            DB.activities,
            q=q,
            cursor=request.args.get("cursor"),
            map_func=lambda doc: activity_from_doc(doc, embed=True),
            col_name="outbox",
        ))

    # Handle POST request aka C2S API
    try:
        _api_required()
    except BadSignature:
        abort(401)

    data = request.get_json(force=True)
    activity = ap.parse_activity(data)
    activity_id = post_to_outbox(activity)

    return Response(status=201, headers={"Location": activity_id})
Esempio n. 18
0
def api_new_note() -> _Response:
    # Basic Micropub (https://www.w3.org/TR/micropub/) query configuration support
    if request.method == "GET" and request.args.get("q") == "config":
        return jsonify({})
    elif request.method == "GET":
        abort(405)

    source = None
    summary = None
    place_tags = []

    # 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 location sent via form-data
        # `geo:28.5,9.0,0.0`
        location = _user_api_arg("location", default="")
        if location.startswith("geo:"):
            slat, slng, *_ = location[4:].split(",")
            place_tags.append(
                {
                    "type": ap.ActivityType.PLACE.value,
                    "url": "",
                    "name": "",
                    "latitude": float(slat),
                    "longitude": float(slng),
                }
            )

        # Handle JSON microformats2 data
        if _user_api_arg("type", default=None):
            _logger.info(f"Micropub request: {request.json}")
            try:
                source = request.json["properties"]["content"][0]
            except (ValueError, KeyError):
                pass

            # Handle HTML
            if isinstance(source, dict):
                source = source.get("html")

            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="")

    if not place_tags:
        if _user_api_arg("location_lat", default=None):
            lat = float(_user_api_arg("location_lat"))
            lng = float(_user_api_arg("location_lng"))
            loc_name = _user_api_arg("location_name", default="")
            place_tags.append(
                {
                    "type": ap.ActivityType.PLACE.value,
                    "url": "",
                    "name": loc_name,
                    "latitude": lat,
                    "longitude": lng,
                }
            )

    # All the following fields are specific to the API (i.e. not Micropub related)
    _reply, reply = None, None
    try:
        _reply = _user_api_arg("reply")
    except ValueError:
        pass

    visibility = ap.Visibility[
        _user_api_arg("visibility", default=ap.Visibility.PUBLIC.name)
    ]

    content, tags = parse_markdown(source)

    # Check for custom emojis
    tags = tags + emojis.tags(content) + place_tags

    to: List[str] = []
    cc: List[str] = []

    if visibility == ap.Visibility.PUBLIC:
        to = [ap.AS_PUBLIC]
        cc = [ID + "/followers"]
    elif visibility == ap.Visibility.UNLISTED:
        to = [ID + "/followers"]
        cc = [ap.AS_PUBLIC]
    elif visibility == ap.Visibility.FOLLOWERS_ONLY:
        to = [ID + "/followers"]
        cc = []

    if _reply:
        reply = ap.fetch_remote_activity(_reply)
        if visibility == ap.Visibility.DIRECT:
            to.append(reply.attributedTo)
        else:
            cc.append(reply.attributedTo)

    context = new_context(reply)

    for tag in tags:
        if tag["type"] == "Mention":
            to.append(tag["href"])

    raw_note = dict(
        attributedTo=MY_PERSON.id,
        cc=list(set(cc) - set([MY_PERSON.id])),
        to=list(set(to) - set([MY_PERSON.id])),
        summary=summary,
        content=content,
        tag=tags,
        source={"mediaType": "text/markdown", "content": source},
        inReplyTo=reply.id if reply else None,
        context=context,
    )

    if request.files:
        for f in request.files.keys():
            if not request.files[f].filename:
                continue

            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)
Esempio n. 19
0
def api_new_note() -> _Response:
    source = _user_api_arg("content")
    if not source:
        raise ValueError("missing content")

    _reply, reply = None, None
    try:
        _reply = _user_api_arg("reply")
    except ValueError:
        pass

    visibility = ap.Visibility[_user_api_arg(
        "visibility", default=ap.Visibility.PUBLIC.name)]

    content, tags = parse_markdown(source)

    to: List[str] = []
    cc: List[str] = []

    if visibility == ap.Visibility.PUBLIC:
        to = [ap.AS_PUBLIC]
        cc = [ID + "/followers"]
    elif visibility == ap.Visibility.UNLISTED:
        to = [ID + "/followers"]
        cc = [ap.AS_PUBLIC]
    elif visibility == ap.Visibility.FOLLOWERS_ONLY:
        to = [ID + "/followers"]
        cc = []

    if _reply:
        reply = ap.fetch_remote_activity(_reply)
        if visibility == ap.Visibility.DIRECT:
            to.append(reply.attributedTo)
        else:
            cc.append(reply.attributedTo)

    for tag in tags:
        if tag["type"] == "Mention":
            if visibility == ap.Visibility.DIRECT:
                to.append(tag["href"])
            else:
                cc.append(tag["href"])

    raw_note = dict(
        attributedTo=MY_PERSON.id,
        cc=list(set(cc)),
        to=list(set(to)),
        content=content,
        tag=tags,
        source={
            "mediaType": "text/markdown",
            "content": source
        },
        inReplyTo=reply.id if reply else None,
    )

    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]

        raw_note["attachment"] = [{
            "mediaType":
            mtype,
            "name":
            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 _user_api_response(activity=create_id)