def migrate(self) -> None: for data in find_activities({ **by_type(ap.ActivityType.CREATE), **not_deleted() }): try: in_reply_to = data["activity"]["object"].get("inReplyTo") if in_reply_to: update_one_activity( by_remote_id(data["remote_id"]), upsert({MetaKey.IN_REPLY_TO: in_reply_to}), ) except Exception: logger.exception(f"failed to process activity {data!r}") for data in DB.replies.find({**not_deleted()}): try: in_reply_to = data["activity"].get("inReplyTo") if in_reply_to: DB.replies.update_one( by_remote_id(data["remote_id"]), upsert({MetaKey.IN_REPLY_TO: in_reply_to}), ) except Exception: logger.exception(f"failed to process activity {data!r}")
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 outbox_activity_replies(item_id): if not is_api_request(): abort(404) _log_sig() data = DB.activities.find_one({ **in_outbox(), **by_remote_id(activity_url(item_id)), **not_deleted(), **is_public(), }) if not data: abort(404) obj = ap.parse_activity(data["activity"]) if obj.ACTIVITY_TYPE != ActivityType.CREATE: abort(404) q = { **is_public(), **not_deleted(), **by_type(ActivityType.CREATE), "activity.object.inReplyTo": obj.get_object().id, } return activitypubify(**activitypub.build_ordered_collection( DB.activities, q=q, cursor=request.args.get("cursor"), map_func=lambda doc: doc["activity"]["object"], col_name=f"outbox/{item_id}/replies", first_page=request.args.get("page") == "first", ))
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 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 migrate(self) -> None: for data in find_activities({ **by_type(ap.ActivityType.CREATE), **not_deleted() }): try: activity = ap.parse_activity(data["activity"]) mentions = [] obj = activity.get_object() for m in obj.get_mentions(): mentions.append(m.href) hashtags = [] for h in obj.get_hashtags(): hashtags.append(h.name[1:]) # Strip the # update_one_activity( by_remote_id(data["remote_id"]), upsert({ MetaKey.MENTIONS: mentions, MetaKey.HASHTAGS: hashtags }), ) except Exception: logger.exception(f"failed to process activity {data!r}")
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 outbox_detail(item_id): if "text/html" in request.headers.get("Accept", ""): return redirect(url_for("note_by_id", note_id=item_id)) doc = DB.activities.find_one({ **in_outbox(), **by_remote_id(activity_url(item_id)), **not_deleted(), **is_public(), }) if not doc: abort(404) _log_sig() if doc["meta"].get("deleted", False): abort(404) return activitypubify(**activity_from_doc(doc))
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 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})
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 _build_thread(data, include_children=True, query=None): # noqa: C901 if query is None: query = {} data["_requested"] = True app.logger.info(f"_build_thread({data!r})") root_id = (data["meta"].get(MetaKey.THREAD_ROOT_PARENT.value) or data["meta"].get(MetaKey.OBJECT_ID.value) or data["remote_id"]) replies = [data] for dat in find_activities({ **by_object_id(root_id), **not_deleted(), **by_type(ap.ActivityType.CREATE), **query, }): replies.append(dat) for dat in find_activities({ **flag(MetaKey.THREAD_ROOT_PARENT, root_id), **not_deleted(), **by_type(ap.ActivityType.CREATE), **query, }): replies.append(dat) for dat in DB.replies.find({ **flag(MetaKey.THREAD_ROOT_PARENT, root_id), **not_deleted(), **query }): # Make a Note/Question/... looks like a Create dat["meta"].update({ MetaKey.OBJECT_VISIBILITY.value: dat["meta"][MetaKey.VISIBILITY.value] }) dat = { "activity": { "object": dat["activity"] }, "meta": dat["meta"], "_id": dat["_id"], } replies.append(dat) replies = sorted(replies, key=lambda d: d["meta"]["published"]) # Index all the IDs in order to build a tree idx = {} replies2 = [] for rep in replies: rep_id = rep["activity"]["object"]["id"] if rep_id in idx: continue idx[rep_id] = rep.copy() idx[rep_id]["_nodes"] = [] replies2.append(rep) # Build the tree for rep in replies2: rep_id = rep["activity"]["object"]["id"] if rep_id == root_id: continue reply_of = ap._get_id(rep["activity"]["object"].get("inReplyTo")) try: idx[reply_of]["_nodes"].append(rep) except KeyError: app.logger.info(f"{reply_of} is not there! skipping {rep}") # Flatten the tree thread = [] def _flatten(node, level=0): node["_level"] = level thread.append(node) for snode in sorted( idx[node["activity"]["object"]["id"]]["_nodes"], key=lambda d: d["activity"]["object"]["published"], ): _flatten(snode, level=level + 1) try: _flatten(idx[root_id]) except KeyError: app.logger.info(f"{root_id} is not there! skipping") return thread
def inject_config(): q = { **in_outbox(), "$or": [ { **by_type(ActivityType.CREATE), **not_deleted(), **by_visibility(ap.Visibility.PUBLIC), }, { **by_type(ActivityType.ANNOUNCE), **not_undo() }, ], } notes_count = DB.activities.count(q) # FIXME(tsileo): rename to all_count, and remove poll answers from it all_q = { **in_outbox(), **by_type([ActivityType.CREATE, ActivityType.ANNOUNCE]), **not_deleted(), **not_undo(), **not_poll_answer(), } liked_q = { **in_outbox(), **by_type(ActivityType.LIKE), **not_undo(), **not_deleted(), } followers_q = { **in_inbox(), **by_type(ActivityType.FOLLOW), **not_undo(), **not_deleted(), } following_q = { **in_outbox(), **by_type(ActivityType.FOLLOW), **follow_request_accepted(), **not_undo(), **not_deleted(), } unread_notifications_q = {_meta(MetaKey.NOTIFICATION_UNREAD): True} logged_in = session.get("logged_in", False) return dict( microblogpub_version=VERSION, config=config, logged_in=logged_in, followers_count=DB.activities.count(followers_q), following_count=DB.activities.count(following_q) if logged_in or not config.HIDE_FOLLOWING else 0, notes_count=notes_count, liked_count=DB.activities.count(liked_q) if logged_in else 0, with_replies_count=DB.activities.count(all_q) if logged_in else 0, unread_notifications_count=DB.activities.count(unread_notifications_q) if logged_in else 0, me=ME, base_url=config.BASE_URL, highlight_css=HIGHLIGHT_CSS, )