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}", ))
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, }), ))
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))
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", ))
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, ))
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, ))
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")
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))
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, ), ))
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(), ))
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))
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, ))
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")
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))
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))
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, ))
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))
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()), ))
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"), ))
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, ))
def admin_lists() -> _Response: lists = list(DB.lists.find()) return htmlify(render_template("lists.html", lists=lists))
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})
def admin_indieauth() -> _Response: return htmlify( render_template( "admin_indieauth.html", indieauth_actions=DB.indieauth.find().sort("ts", -1).limit(100), ))
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, ))