Esempio n. 1
0
def tags(tag):
    if not DB.activities.count({
            **in_outbox(),
            **by_hashtag(tag),
            **by_visibility(ap.Visibility.PUBLIC),
            **not_deleted(),
    }):
        abort(404)
    if not is_api_request():
        return htmlify(
            render_template(
                "tags.html",
                tag=tag,
                outbox_data=DB.activities.find({
                    **in_outbox(),
                    **by_hashtag(tag),
                    **by_visibility(ap.Visibility.PUBLIC),
                    **not_deleted(),
                }).sort("meta.published", -1),
            ))
    _log_sig()
    q = {
        **in_outbox(),
        **by_hashtag(tag),
        **by_visibility(ap.Visibility.PUBLIC),
        **not_deleted(),
    }
    return activitypubify(**activitypub.build_ordered_collection(
        DB.activities,
        q=q,
        cursor=request.args.get("cursor"),
        map_func=lambda doc: doc["activity"]["object"]["id"],
        col_name=f"tags/{tag}",
    ))
Esempio n. 2
0
def admin_index() -> _Response:
    q = {
        "meta.deleted": False,
        "meta.undo": False,
        "type": ap.ActivityType.LIKE.value,
        "box": Box.OUTBOX.value,
    }
    col_liked = DB.activities.count(q)

    return htmlify(
        render_template(
            "admin.html",
            instances=list(DB.instances.find()),
            inbox_size=DB.activities.count({"box": Box.INBOX.value}),
            outbox_size=DB.activities.count({"box": Box.OUTBOX.value}),
            col_liked=col_liked,
            col_followers=DB.activities.count({
                "box": Box.INBOX.value,
                "type": ap.ActivityType.FOLLOW.value,
                "meta.undo": False,
            }),
            col_following=DB.activities.count({
                "box": Box.OUTBOX.value,
                "type": ap.ActivityType.FOLLOW.value,
                "meta.undo": False,
            }),
        ))
Esempio n. 3
0
def admin_thread() -> _Response:
    oid = request.args.get("oid")
    if not oid:
        abort(404)

    data = find_one_activity({
        **by_type(ap.ActivityType.CREATE),
        **by_object_id(oid)
    })
    if not data:
        dat = DB.replies.find_one({**by_remote_id(oid)})
        data = {
            "activity": {
                "object": dat["activity"]
            },
            "meta": dat["meta"],
            "_id": dat["_id"],
        }

    if not data:
        abort(404)
    if data["meta"].get("deleted", False):
        abort(410)
    thread = _build_thread(data)

    tpl = "note.html"
    if request.args.get("debug"):
        tpl = "note_debug.html"
    return htmlify(render_template(tpl, thread=thread, note=data))
Esempio n. 4
0
def liked():
    if not is_api_request():
        q = {
            "box": Box.OUTBOX.value,
            "type": ActivityType.LIKE.value,
            "meta.deleted": False,
            "meta.undo": False,
        }

        liked, older_than, newer_than = paginated_query(DB.activities, q)

        return htmlify(
            render_template("liked.html",
                            liked=liked,
                            older_than=older_than,
                            newer_than=newer_than))

    q = {
        "meta.deleted": False,
        "meta.undo": False,
        "type": ActivityType.LIKE.value
    }
    return activitypubify(**activitypub.build_ordered_collection(
        DB.activities,
        q=q,
        cursor=request.args.get("cursor"),
        map_func=lambda doc: doc["activity"]["object"],
        col_name="liked",
    ))
Esempio n. 5
0
def followers():
    q = {
        "box": Box.INBOX.value,
        "type": ActivityType.FOLLOW.value,
        "meta.undo": False
    }

    if is_api_request():
        _log_sig()
        return activitypubify(**activitypub.build_ordered_collection(
            DB.activities,
            q=q,
            cursor=request.args.get("cursor"),
            map_func=lambda doc: doc["activity"]["actor"],
            col_name="followers",
        ))

    raw_followers, older_than, newer_than = paginated_query(DB.activities, q)
    followers = [
        doc["meta"] for doc in raw_followers if "actor" in doc.get("meta", {})
    ]
    return htmlify(
        render_template(
            "followers.html",
            followers_data=followers,
            older_than=older_than,
            newer_than=newer_than,
        ))
Esempio n. 6
0
def admin_list(name: str) -> _Response:
    list_ = DB.lists.find_one({"name": name})
    if not list_:
        abort(404)

    q = {
        "meta.stream": True,
        "meta.deleted": False,
        "meta.actor_id": {
            "$in": list_["members"]
        },
    }

    tpl = "stream.html"
    if request.args.get("debug"):
        tpl = "stream_debug.html"
        if request.args.get("debug_inbox"):
            q = {}

    inbox_data, older_than, newer_than = paginated_query(
        DB.activities, q, limit=int(request.args.get("limit", 25)))

    return htmlify(
        render_template(
            tpl,
            inbox_data=inbox_data,
            older_than=older_than,
            newer_than=newer_than,
            list_name=name,
        ))
Esempio n. 7
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. 8
0
def note_by_id(note_id):
    if is_api_request():
        return redirect(url_for("outbox_activity", item_id=note_id))

    query = {}
    # Prevent displaying direct messages on the public frontend
    if not session.get("logged_in", False):
        query = is_public()

    data = DB.activities.find_one({
        **in_outbox(),
        **by_remote_id(activity_url(note_id)),
        **query
    })
    if not data:
        abort(404)
    if data["meta"].get("deleted", False):
        abort(410)

    thread = _build_thread(data, query=query)
    app.logger.info(f"thread={thread!r}")

    raw_likes = list(
        DB.activities.find({
            **not_undo(),
            **not_deleted(),
            **by_type(ActivityType.LIKE),
            **by_object_id(data["activity"]["object"]["id"]),
        }))
    likes = []
    for doc in raw_likes:
        try:
            likes.append(doc["meta"]["actor"])
        except Exception:
            app.logger.exception(f"invalid doc: {doc!r}")
    app.logger.info(f"likes={likes!r}")

    raw_shares = list(
        DB.activities.find({
            **not_undo(),
            **not_deleted(),
            **by_type(ActivityType.ANNOUNCE),
            **by_object_id(data["activity"]["object"]["id"]),
        }))
    shares = []
    for doc in raw_shares:
        try:
            shares.append(doc["meta"]["actor"])
        except Exception:
            app.logger.exception(f"invalid doc: {doc!r}")
    app.logger.info(f"shares={shares!r}")

    return htmlify(
        render_template("note.html",
                        likes=likes,
                        shares=shares,
                        thread=thread,
                        note=data))
Esempio n. 9
0
def admin_new() -> _Response:
    reply_id = None
    content = ""
    thread: List[Any] = []
    print(request.args)
    default_visibility = None  # ap.Visibility.PUBLIC
    if request.args.get("reply"):
        data = DB.activities.find_one(
            {"activity.object.id": request.args.get("reply")})
        if data:
            reply = ap.parse_activity(data["activity"])
        else:
            obj = ap.get_backend().fetch_iri(request.args.get("reply"))
            data = dict(meta=_meta(ap.parse_activity(obj)),
                        activity=dict(object=obj))
            data["_id"] = obj["id"]
            data["remote_id"] = obj["id"]
            reply = ap.parse_activity(data["activity"]["object"])
        # Fetch the post visibility, in case it's follower only
        default_visibility = ap.get_visibility(reply)
        # If it's public, we default the reply to unlisted
        if default_visibility == ap.Visibility.PUBLIC:
            default_visibility = ap.Visibility.UNLISTED

        reply_id = reply.id
        if reply.ACTIVITY_TYPE == ap.ActivityType.CREATE:
            reply_id = reply.get_object().id

        actor = reply.get_actor()
        domain = urlparse(actor.id).netloc
        # FIXME(tsileo): if reply of reply, fetch all participants
        content = f"@{actor.preferredUsername}@{domain} "
        if reply.has_type(ap.ActivityType.CREATE):
            reply = reply.get_object()
        for mention in reply.get_mentions():
            if mention.href in [actor.id, ID]:
                continue
            m = ap.fetch_remote_activity(mention.href)
            if m.has_type(ap.ACTOR_TYPES):
                d = urlparse(m.id).netloc
                content += f"@{m.preferredUsername}@{d} "

        thread = _build_thread(data)

    return htmlify(
        render_template(
            "new.html",
            reply=reply_id,
            content=content,
            thread=thread,
            default_visibility=default_visibility,
            visibility=ap.Visibility,
            emojis=config.EMOJIS.split(" "),
            custom_emojis=sorted(
                [ap.Emoji(**dat) for name, dat in EMOJIS_BY_NAME.items()],
                key=lambda e: e.name,
            ),
        ))
Esempio n. 10
0
def admin_tasks() -> _Response:
    return htmlify(
        render_template(
            "admin_tasks.html",
            success=p.get_success(),
            dead=p.get_dead(),
            waiting=p.get_waiting(),
            cron=p.get_cron(),
        ))
Esempio n. 11
0
def remote_follow():
    """Form to allow visitor to perform the remote follow dance."""
    if request.method == "GET":
        return htmlify(render_template("remote_follow.html"))

    csrf.protect()
    profile = request.form.get("profile")
    if not profile.startswith("@"):
        profile = f"@{profile}"
    return redirect(get_remote_follow_template(profile).format(uri=ID))
Esempio n. 12
0
def index():
    if is_api_request():
        _log_sig()
        return activitypubify(**ME)

    q = {
        **in_outbox(),
        "$or": [
            {
                **by_type(ActivityType.CREATE),
                **not_deleted(),
                **by_visibility(ap.Visibility.PUBLIC),
                "$or": [{
                    "meta.pinned": False
                }, {
                    "meta.pinned": {
                        "$exists": False
                    }
                }],
            },
            {
                **by_type(ActivityType.ANNOUNCE),
                **not_undo()
            },
        ],
    }

    apinned = []
    # Only fetch the pinned notes if we're on the first page
    if not request.args.get("older_than") and not request.args.get(
            "newer_than"):
        q_pinned = {
            **in_outbox(),
            **by_type(ActivityType.CREATE),
            **not_deleted(),
            **pinned(),
            **by_visibility(ap.Visibility.PUBLIC),
        }
        apinned = list(DB.activities.find(q_pinned))

    outbox_data, older_than, newer_than = paginated_query(DB.activities,
                                                          q,
                                                          limit=25 -
                                                          len(apinned))

    return htmlify(
        render_template(
            "index.html",
            outbox_data=outbox_data,
            older_than=older_than,
            newer_than=newer_than,
            pinned=apinned,
        ))
Esempio n. 13
0
def u2f_register():
    # TODO(tsileo): ensure no duplicates
    if request.method == "GET":
        payload = u2f.begin_registration(ID)
        session["challenge"] = payload
        return htmlify(render_template("u2f.html", payload=payload))
    else:
        resp = json.loads(request.form.get("resp"))
        device, device_cert = u2f.complete_registration(
            session["challenge"], resp)
        session["challenge"] = None
        DB.u2f.insert_one({"device": device, "cert": device_cert})
        session["logged_in"] = False
        return redirect("/login")
Esempio n. 14
0
def admin_direct_messages() -> _Response:
    all_dms = DB.activities.find({
        **not_poll_answer(),
        **by_type(ap.ActivityType.CREATE),
        **by_object_visibility(ap.Visibility.DIRECT),
    }).sort("meta.published", -1)

    # Group by threads
    _threads = defaultdict(list)  # type: ignore
    for dm in all_dms:
        # Skip poll answers
        if dm["activity"].get("object", {}).get("name"):
            continue

        _threads[dm["meta"].get("thread_root_parent",
                                dm["meta"]["object_id"])].append(dm)

    # Now build the data needed for the UI
    threads = []
    for thread_root, thread in _threads.items():
        # We need the list of participants
        participants = set()
        for raw_activity in thread:
            activity = ap.parse_activity(raw_activity["activity"])
            actor = activity.get_actor()
            domain = urlparse(actor.id).netloc
            if actor.id != ID:
                participants.add(f"@{actor.preferredUsername}@{domain}")
            if activity.has_type(ap.ActivityType.CREATE):
                activity = activity.get_object()
            for mention in activity.get_mentions():
                if mention.href in [actor.id, ID]:
                    continue
                m = ap.fetch_remote_activity(mention.href)
                if m.has_type(ap.ACTOR_TYPES) and m.id != ID:
                    d = urlparse(m.id).netloc
                    participants.add(f"@{m.preferredUsername}@{d}")

            if not participants:
                continue
        # Build the UI data for this conversation
        oid = thread[-1]["meta"]["object_id"]
        threads.append({
            "participants": list(participants),
            "oid": oid,
            "last_reply": thread[0],
            "len": len(thread),
        })
    return htmlify(render_template("direct_messages.html", threads=threads))
Esempio n. 15
0
def admin_login() -> _Response:
    if session.get("logged_in") is True:
        return redirect(url_for("admin.admin_notifications"))

    devices = [doc["device"] for doc in DB.u2f.find()]
    u2f_enabled = True if devices else False
    if request.method == "POST":
        csrf.protect()
        # 1. Check regular password login flow
        pwd = request.form.get("pass")
        if pwd:
            if verify_pass(pwd):
                session.permanent = True
                session["logged_in"] = True
                return redirect(
                    request.args.get("redirect")
                    or url_for("admin.admin_notifications"))
            else:
                abort(403)
        # 2. Check for U2F payload, if any
        elif devices:
            resp = json.loads(request.form.get("resp"))  # type: ignore
            try:
                u2f.complete_authentication(session["challenge"], resp)
            except ValueError as exc:
                print("failed", exc)
                abort(403)
                return
            finally:
                session["challenge"] = None

            session.permanent = True
            session["logged_in"] = True
            return redirect(
                request.args.get("redirect")
                or url_for("admin.admin_notifications"))
        else:
            abort(401)

    payload = None
    if devices:
        payload = u2f.begin_authentication(ID, devices)
        session["challenge"] = payload

    return htmlify(
        render_template("login.html", u2f_enabled=u2f_enabled,
                        payload=payload))
Esempio n. 16
0
def all():
    q = {
        **in_outbox(),
        **by_type([ActivityType.CREATE, ActivityType.ANNOUNCE]),
        **not_deleted(),
        **not_undo(),
        **not_poll_answer(),
    }
    outbox_data, older_than, newer_than = paginated_query(DB.activities, q)

    return htmlify(
        render_template(
            "index.html",
            outbox_data=outbox_data,
            older_than=older_than,
            newer_than=newer_than,
        ))
Esempio n. 17
0
def admin_bookmarks() -> _Response:
    q = {"meta.bookmarked": True}

    tpl = "stream.html"
    if request.args.get("debug"):
        tpl = "stream_debug.html"
        if request.args.get("debug_inbox"):
            q = {}

    inbox_data, older_than, newer_than = paginated_query(
        DB.activities, q, limit=int(request.args.get("limit", 25)))

    return htmlify(
        render_template(tpl,
                        inbox_data=inbox_data,
                        older_than=older_than,
                        newer_than=newer_than))
Esempio n. 18
0
def admin_profile() -> _Response:
    if not request.args.get("actor_id"):
        abort(404)

    actor_id = request.args.get("actor_id")
    actor = ap.fetch_remote_activity(actor_id)
    q = {
        "meta.actor_id": actor_id,
        "box": "inbox",
        **not_deleted(),
        "type": {
            "$in":
            [ap.ActivityType.CREATE.value, ap.ActivityType.ANNOUNCE.value]
        },
    }
    inbox_data, older_than, newer_than = paginated_query(
        DB.activities, q, limit=int(request.args.get("limit", 25)))
    follower = find_one_activity({
        "box": "inbox",
        "type": ap.ActivityType.FOLLOW.value,
        "meta.actor_id": actor.id,
        "meta.undo": False,
    })
    following = find_one_activity({
        **by_type(ap.ActivityType.FOLLOW),
        **by_object_id(actor.id),
        **not_undo(),
        **in_outbox(),
        **follow_request_accepted(),
    })

    return htmlify(
        render_template(
            "stream.html",
            actor_id=actor_id,
            actor=actor.to_dict(),
            inbox_data=inbox_data,
            older_than=older_than,
            newer_than=newer_than,
            follower=follower,
            following=following,
            lists=list(DB.lists.find()),
        ))
Esempio n. 19
0
def admin_lookup() -> _Response:
    data = None
    meta = None
    follower = None
    following = None
    if request.args.get("url"):
        data = lookup(request.args.get("url"))  # type: ignore
        if data:
            if not data.has_type(ap.ACTOR_TYPES):
                meta = _meta(data)
            else:
                follower = find_one_activity({
                    "box": "inbox",
                    "type": ap.ActivityType.FOLLOW.value,
                    "meta.actor_id": data.id,
                    "meta.undo": False,
                })
                following = find_one_activity({
                    **by_type(ap.ActivityType.FOLLOW),
                    **by_object_id(data.id),
                    **not_undo(),
                    **in_outbox(),
                    **follow_request_accepted(),
                })

            if data.has_type(ap.ActivityType.QUESTION):
                p.push(data.id, "/task/fetch_remote_question")

        print(data)
        app.logger.debug(data.to_dict())
    return htmlify(
        render_template(
            "lookup.html",
            data=data,
            meta=meta,
            follower=follower,
            following=following,
            url=request.args.get("url"),
        ))
Esempio n. 20
0
def following():
    q = {
        **in_outbox(),
        **by_type(ActivityType.FOLLOW),
        **not_deleted(),
        **follow_request_accepted(),
        **not_undo(),
    }

    if is_api_request():
        _log_sig()
        if config.HIDE_FOLLOWING:
            return activitypubify(
                **activitypub.simple_build_ordered_collection("following", []))

        return activitypubify(**activitypub.build_ordered_collection(
            DB.activities,
            q=q,
            cursor=request.args.get("cursor"),
            map_func=lambda doc: doc["activity"]["object"],
            col_name="following",
        ))

    if config.HIDE_FOLLOWING and not session.get("logged_in", False):
        abort(404)

    following, older_than, newer_than = paginated_query(DB.activities, q)
    following = [(doc["remote_id"], doc["meta"]) for doc in following
                 if "remote_id" in doc and "object" in doc.get("meta", {})]
    lists = list(DB.lists.find())
    return htmlify(
        render_template(
            "following.html",
            following_data=following,
            older_than=older_than,
            newer_than=newer_than,
            lists=lists,
        ))
Esempio n. 21
0
def admin_lists() -> _Response:
    lists = list(DB.lists.find())

    return htmlify(render_template("lists.html", lists=lists))
Esempio n. 22
0
def indieauth_endpoint():
    if request.method == "GET":
        if not session.get("logged_in"):
            return redirect(url_for("admin.admin_login", redirect=request.url))

        me = request.args.get("me")
        # FIXME(tsileo): ensure me == ID
        client_id = request.args.get("client_id")
        redirect_uri = request.args.get("redirect_uri")
        state = request.args.get("state", "")
        response_type = request.args.get("response_type", "id")
        scope = request.args.get("scope", "").split()

        print("STATE", state)
        return htmlify(
            render_template(
                "indieauth_flow.html",
                client=get_client_id_data(client_id),
                scopes=scope,
                redirect_uri=redirect_uri,
                state=state,
                response_type=response_type,
                client_id=client_id,
                me=me,
            ))

    # Auth verification via POST
    code = request.form.get("code")
    redirect_uri = request.form.get("redirect_uri")
    client_id = request.form.get("client_id")

    ip, geoip = _get_ip()

    auth = DB.indieauth.find_one_and_update(
        {
            "code": code,
            "redirect_uri": redirect_uri,
            "client_id": client_id,
            "verified": False,
        },
        {
            "$set": {
                "verified": True,
                "verified_by": "id",
                "verified_at": datetime.now().timestamp(),
                "ip_address": ip,
                "geoip": geoip,
            }
        },
    )
    print(auth)
    print(code, redirect_uri, client_id)

    # Ensure the code is recent
    if (datetime.now() -
            datetime.fromtimestamp(auth["ts"])) > timedelta(minutes=5):
        abort(400)

    if not auth:
        abort(403)
        return

    session["logged_in"] = True
    me = auth["me"]
    state = auth["state"]
    scope = auth["scope"]
    print("STATE", state)
    return build_auth_resp({"me": me, "state": state, "scope": scope})
Esempio n. 23
0
def admin_indieauth() -> _Response:
    return htmlify(
        render_template(
            "admin_indieauth.html",
            indieauth_actions=DB.indieauth.find().sort("ts", -1).limit(100),
        ))
Esempio n. 24
0
def admin_notifications() -> _Response:
    # Setup the cron for deleting old activities

    # FIXME(tsileo): put back to 12h
    p.push({}, "/task/cleanup", schedule="@every 1h")

    # Trigger a cleanup if asked
    if request.args.get("cleanup"):
        p.push({}, "/task/cleanup")

    # FIXME(tsileo): show unfollow (performed by the current actor) and liked???
    mentions_query = {
        "type": ap.ActivityType.CREATE.value,
        "activity.object.tag.type": "Mention",
        "activity.object.tag.name": f"@{config.USERNAME}@{config.DOMAIN}",
        "meta.deleted": False,
    }
    replies_query = {
        "type": ap.ActivityType.CREATE.value,
        "activity.object.inReplyTo": {
            "$regex": f"^{config.BASE_URL}"
        },
        "meta.poll_answer": False,
    }
    announced_query = {
        "type": ap.ActivityType.ANNOUNCE.value,
        "activity.object": {
            "$regex": f"^{config.BASE_URL}"
        },
    }
    new_followers_query = {"type": ap.ActivityType.FOLLOW.value}
    unfollow_query = {
        "type": ap.ActivityType.UNDO.value,
        "activity.object.type": ap.ActivityType.FOLLOW.value,
    }
    likes_query = {
        "type": ap.ActivityType.LIKE.value,
        "activity.object": {
            "$regex": f"^{config.BASE_URL}"
        },
    }
    followed_query = {"type": ap.ActivityType.ACCEPT.value}
    rejected_query = {"type": ap.ActivityType.REJECT.value}
    q = {
        "box":
        Box.INBOX.value,
        "$or": [
            mentions_query,
            announced_query,
            replies_query,
            new_followers_query,
            followed_query,
            rejected_query,
            unfollow_query,
            likes_query,
        ],
    }
    inbox_data, older_than, newer_than = paginated_query(DB.activities, q)
    if not newer_than:
        nstart = datetime.now(timezone.utc).isoformat()
    else:
        nstart = inbox_data[0]["_id"].generation_time.isoformat()
    if not older_than:
        nend = (datetime.now(timezone.utc) - timedelta(days=15)).isoformat()
    else:
        nend = inbox_data[-1]["_id"].generation_time.isoformat()
    print(nstart, nend)
    notifs = list(
        DB.notifications.find({
            "datetime": {
                "$lte": nstart,
                "$gt": nend
            }
        }).sort("_id", -1).limit(50))
    print(inbox_data)

    nid = None
    if inbox_data:
        nid = inbox_data[0]["_id"]

    inbox_data.extend(notifs)
    inbox_data = sorted(inbox_data,
                        reverse=True,
                        key=lambda doc: doc["_id"].generation_time)

    return htmlify(
        render_template(
            "stream.html",
            inbox_data=inbox_data,
            older_than=older_than,
            newer_than=newer_than,
            nid=nid,
        ))