Esempio n. 1
0
def fetch_og_metadata(self, iri: str) -> None:
    try:
        activity = ap.fetch_remote_activity(iri)
        log.info(f"activity={activity!r}")
        if activity.has_type(ap.ActivityType.CREATE):
            note = activity.get_object()
            links = opengraph.links_from_note(note.to_dict())
            og_metadata = opengraph.fetch_og_metadata(USER_AGENT, links)
            for og in og_metadata:
                if not og.get("image"):
                    continue
                MEDIA_CACHE.cache_og_image(og["image"])

            log.debug(f"OG metadata {og_metadata!r}")
            DB.activities.update_one(
                {"remote_id": iri},
                {"$set": {
                    "meta.og_metadata": og_metadata
                }})

        log.info(f"OG metadata fetched for {iri}")
    except (ActivityGoneError, ActivityNotFoundError):
        log.exception(f"dropping activity {iri}, skip OG metedata")
    except requests.exceptions.HTTPError as http_err:
        if 400 <= http_err.response.status_code < 500:
            log.exception("bad request, no retry")
            return
        log.exception("failed to fetch OG metadata")
        self.retry(exc=http_err,
                   countdown=int(random.uniform(2, 4)**self.request.retries))
    except Exception as err:
        log.exception(f"failed to fetch OG metadata for {iri}")
        self.retry(exc=err,
                   countdown=int(random.uniform(2, 4)**self.request.retries))
Esempio n. 2
0
def task_cache_emoji() -> _Response:
    task = p.parse(flask.request)
    app.logger.info(f"task={task!r}")
    iri = task.payload["iri"]
    url = task.payload["url"]
    try:
        MEDIA_CACHE.cache_emoji(url, iri)
    except Exception as exc:
        err = f"failed to cache emoji {url} at {iri}"
        app.logger.exception(err)
        raise TaskError() from exc

    return ""
Esempio n. 3
0
def task_cache_actor_icon() -> _Response:
    task = p.parse(flask.request)
    app.logger.info(f"task={task!r}")
    actor_iri = task.payload["actor_iri"]
    icon_url = task.payload["icon_url"]
    try:
        MEDIA_CACHE.cache_actor_icon(icon_url)
    except Exception as exc:
        err = f"failed to cache actor icon {icon_url} for {actor_iri}"
        app.logger.exception(err)
        raise TaskError() from exc

    return ""
Esempio n. 4
0
    def cache_actor_icon(icon_url: str, actor_iri: str) -> None:
        if MEDIA_CACHE.is_actor_icon_cached(icon_url):
            return None

        p.push({
            "icon_url": icon_url,
            "actor_iri": actor_iri
        }, "/task/cache_actor_icon")
Esempio n. 5
0
def cache_attachments(self, iri: str) -> None:
    try:
        activity = ap.fetch_remote_activity(iri)
        log.info(f"activity={activity!r}")
        # Generates thumbnails for the actor's icon and the attachments if any

        actor = activity.get_actor()

        # Update the cached actor
        DB.actors.update_one(
            {"remote_id": iri},
            {"$set": {
                "remote_id": iri,
                "data": actor.to_dict(embed=True)
            }},
            upsert=True,
        )

        if actor.icon:
            MEDIA_CACHE.cache(actor.icon["url"], Kind.ACTOR_ICON)

        if activity.has_type(ap.ActivityType.CREATE):
            for attachment in activity.get_object()._data.get(
                    "attachment", []):
                if (attachment.get("mediaType", "").startswith("image/") or
                        attachment.get("type") == ap.ActivityType.IMAGE.value):
                    try:
                        MEDIA_CACHE.cache(attachment["url"], Kind.ATTACHMENT)
                    except ValueError:
                        log.exception(f"failed to cache {attachment}")

        log.info(f"attachments cached for {iri}")

    except (ActivityGoneError, ActivityNotFoundError, NotAnActivityError):
        log.exception(f"dropping activity {iri}, no attachment caching")
    except Exception as err:
        log.exception(f"failed to cache attachments for {iri}")
        self.retry(exc=err,
                   countdown=int(random.uniform(2, 4)**self.request.retries))
Esempio n. 6
0
def _get_file_url(url, size, kind):
    k = (kind, url, size)
    cached = _GRIDFS_CACHE.get(k)
    if cached:
        return cached

    doc = MEDIA_CACHE.get_file(url, size, kind)
    if doc:
        u = f"/media/{str(doc._id)}"
        _GRIDFS_CACHE[k] = u
        return u

    # MEDIA_CACHE.cache(url, kind)
    _logger.error(f"cache not available for {url}/{size}/{kind}")
    return url
Esempio n. 7
0
def _get_file_url(url, size, kind) -> str:
    k = (url, size, kind)
    cached = _FILE_URL_CACHE.get(k)
    if cached:
        return cached

    doc = MEDIA_CACHE.get_file(*k)
    if doc:
        out = f"/media/{str(doc._id)}"
        _FILE_URL_CACHE[k] = out
        return out

    _logger.error(f"cache not available for {url}/{size}/{kind}")
    if url.startswith(BASE_URL):
        return url

    p = urlparse(url)
    return f"/p/{p.scheme}" + p._replace(scheme="").geturl()[1:]
Esempio n. 8
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. 9
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)
Esempio n. 10
0
    def cache_emoji(url: str, iri: str) -> None:
        if MEDIA_CACHE.is_emoji_cached(iri):
            return None

        p.push({"url": url, "iri": iri}, "/task/cache_emoji")
Esempio n. 11
0
 def cache_actor_icon(icon_url: str, actor_iri: str) -> None:
     if MEDIA_CACHE.is_actor_icon_cached(icon_url):
         return None