{ "cells": [ { "cell_type": "markdown", "id": "0cd4388e-b71c-4bd1-a60e-d2ef3933f3de", "metadata": {}, "source": [ "# Demo: Display Real Bluesky Comments and Replies" ] }, { "cell_type": "markdown", "id": "123456789-930485093240532940945-0324095320945904325", "metadata": { "tags": [] }, "source": [" _Choose Social Media Platform: Reddit | Discord | __Bluesky__ | No Coding_ "] }, { "cell_type": "markdown", "id": "5fdfe7cf-3641-4919-9d56-f3b8c74b7b96", "metadata": {}, "source": [ "Now lets do the same thing we did on the last page (using recursion to display comments and replies), but do it on Bluesky! (Either for real or faked with the fake_atproto library)." ] }, { "cell_type": "markdown", "id": "cb900b5a-c199-47b8-95a9-417d44a786ab", "metadata": { "tags": [] }, "source": [ "## Normal Bluesky Setup\n", "\n", "We'll start by doing our normal steps including these helper functions:" ] }, { "cell_type": "markdown", "id": "694c9604-77f1-43ea-b539-5189824b801b", "metadata": {}, "source": [ "### helper function for atproto links\n", "_NOTE: You don't need to worry about the details of how this works, it just is here to make the code later easier to use._" ] }, { "cell_type": "code", "execution_count": null, "id": "a6d1de73-748c-44ab-acd5-2f1b1d90fef2", "metadata": {}, "outputs": [], "source": [ "import re #load a \"regular expression\" library for helping to parse text\n", "from atproto import IdResolver # Load the atproto IdResolver library to get offical ATProto user IDs\n", "\n", "def get_at_post_link_from_url(url):\n", " # Initialize and log in with the client\n", "\n", " # Extract username and post ID from the URL\n", " match = re.search(r'https://bsky.app/profile/([^/]+)/post/([^/]+)', url)\n", " if not match:\n", " raise ValueError(\"Invalid Bluesky post URL format.\")\n", " user_handle, post_id = match.groups()\n", "\n", " # Construct the at:// URI\n", " post_uri = f\"at://{user_handle}/app.bsky.feed.post/{post_id}\"\n", "\n", " return post_uri\n", "\n", "# function to convert a feed from a weblink url to the special atproto \"at\" URI\n", "def getATFeedLinkFromURL(url):\n", " \n", " # Get the user did and feed id from the weblink url\n", " match = re.search(r'https://bsky.app/profile/([^/]+)/feed/([^/]+)', url)\n", " if not match:\n", " raise ValueError(\"Invalid Bluesky feed URL format.\")\n", " user_handle, feed_id = match.groups()\n", "\n", " # Get the official atproto user ID (did) from the handle\n", " resolver = IdResolver()\n", " did = resolver.handle.resolve(user_handle)\n", " if not did:\n", " raise ValueError(f'Could not resolve DID for handle \"{user_handle}\".')\n", "\n", " # Construct the at:// URI\n", " post_uri = f\"at://{did}/app.bsky.feed.generator/{feed_id}\"\n" ] }, { "cell_type": "markdown", "id": "870646df-edb0-4d99-a5d1-3376416000b4", "metadata": {}, "source": [ "Now we can continue logging in to Bluesky and look through multiple posts.\n", "### load atproto library" ] }, { "cell_type": "code", "execution_count": null, "id": "883641dc-a5a4-47d7-ba9c-3532e9df58d8", "metadata": {}, "outputs": [], "source": [ "# Load some code called \"Client\" from the \"atproto\" library that will help us work with Bluesky\n", "from atproto import Client" ] }, { "cell_type": "markdown", "id": "cc8ae3f6", "metadata": {}, "source": [ "### (optional) make a fake Bluesky connection with the fake_atproto library\n", "For testing purposes, we\"ve added this line of code, which loads a fake version of atproto, so it wont actually connect to Bluesky. __If you want to try to actually connect to Bluesky, don't run this line of code.__" ] }, { "cell_type": "code", "execution_count": null, "id": "d42b7010-2690-4efb-b404-02682e39b559", "metadata": {}, "outputs": [], "source": [ "%run ../../fake_apis/fake_atproto.ipynb" ] }, { "cell_type": "markdown", "id": "45e12f6e-0160-46dc-b382-cbc698d08f58", "metadata": {}, "source": [ "### login to Bluesky" ] }, { "cell_type": "code", "execution_count": null, "id": "a45ce757-b313-4de6-b5c4-32d255946f0f", "metadata": {}, "outputs": [], "source": [ "# Login to Bluesky\n", "# TODO: put your account name and password below\n", "\n", "client = Client(base_url=\"https://bsky.social\")\n", "client.login(\"your_account_name.bsky.social\", \"m#5@_fake_bsky_password_$%Ds\")" ] }, { "cell_type": "markdown", "id": "da45bc67-b72b-45ef-8f70-7eedf9261945", "metadata": {}, "source": [ "## Helper function to display text in an indented box\n", "(You don't need to worry about how this works. This is that function that helps display posts in indented boxes)" ] }, { "cell_type": "code", "execution_count": null, "id": "98574fd2-9380-4653-8b42-d5f00fd634c3", "metadata": {}, "outputs": [], "source": [ "from IPython.display import HTML, Image, display\n", "import html\n", "def display_indented(text, left_margin=0):\n", " display(\n", " HTML(\n", " \"
\" + \n",
    "            html.escape(text) + \n",
    "            \"
\"\n", " )\n", " )" ] }, { "cell_type": "markdown", "id": "d37b46e5-c941-4ac9-a95f-a16061ecf7cf", "metadata": { "tags": [] }, "source": [ "## Code to print a post with replies\n", "\n", "The `print_post_thread` is a function that takes a Bluesky Post weblink (url) (instructions on where to get one below), downloads the thread that follows that post, and then uses the `print_post_and_replies` function to print out that post and the replies to that post." ] }, { "cell_type": "code", "execution_count": null, "id": "f80fa629-1b46-4287-8f51-0dadc13532e5", "metadata": {}, "outputs": [], "source": [ "def print_post_thread(postUrl):\n", "\n", " at_post_link = get_at_post_link_from_url(postUrl)\n", " \n", " # Fetch the post details\n", " post_data = client.get_post_thread(at_post_link)\n", " \n", " print_post_and_replies(post_data.thread)" ] }, { "cell_type": "markdown", "id": "6d28fb60-3e2b-40e1-a3cb-c3b4afac9649", "metadata": {}, "source": [ "The `print_post_and_replies` function takes a given post and recursively prints that post as well as all replies to that post (which will also print all the replies to those replies, etc.)" ] }, { "cell_type": "code", "execution_count": null, "id": "d0b83265-3a27-47a3-a68a-5d3293ed7a3d", "metadata": {}, "outputs": [], "source": [ "def print_post_and_replies(postInfo, num_indents=0):\n", " \n", " # make sure this post isn't blocked (since we can't read blocked posts)\n", " if not (hasattr(postInfo,'blocked') and postInfo.blocked):\n", " \n", " post = postInfo.post\n", " replies = postInfo.replies\n", "\n", " # If replies is None, make it an empty array (so the for loop later doesn't crash)\n", " if not replies:\n", " replies = []\n", " \n", " display_text = (\n", " post.record.text + \"\\n\" +\n", " \"-- \" + str(post.author.display_name) + \" (\" + str(post.author.handle) + \")\\n\" + \n", " \" (likes: \" + str(post.like_count) + \n", " \", replies: \" + str(post.reply_count) +\n", " \", reposts: \" + str(post.repost_count) + \n", " \", quotes: \" + str(post.quote_count) + \") - \" \n", " )\n", " \n", " display_indented(display_text, num_indents*20)\n", " \n", " #print replies (and the replies of those, etc.)\n", " for reply in replies:\n", " print_post_and_replies(reply, num_indents = num_indents + 1)" ] }, { "cell_type": "markdown", "id": "b8d5835b-ef1d-45a3-a101-065e94cc0d17", "metadata": {}, "source": [ "## Finding post IDs and testing our code\n", "In order to test it out, we need to find a link to a Bluesky post with replies. Once you find the post, find the 'Copy Link to Post' option to get a web url for the post.\n", "\n", "![Bluesky Post. The three dot \"Open post options menu\" is opened, and from there the \"Copy Link to Post\" option is selected.](bsky_copy_link.png)\n", "\n", "It should be something like: [https://bsky.app/profile/realgdt.bsky.social/post/3lihunicmds2y](https://bsky.app/profile/realgdt.bsky.social/post/3lihunicmds2y)\n", "\n", "Now we can test it out by calling the `print_post_thread`, passing it the url as a string. Then you should see the comment tree." ] }, { "cell_type": "code", "execution_count": null, "id": "cb70bbcf-2585-426e-b5ff-55ca84a1f35d", "metadata": {}, "outputs": [], "source": [ "print_post_thread('https://bsky.app/profile/realgdt.bsky.social/post/3lihunicmds2y')" ] } ], "metadata": { "kernelspec": { "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.12.5" } }, "nbformat": 4, "nbformat_minor": 5 }