<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>Media on LNA-DEV ~ Lukas Nagel</title>
    <link>https://lna-dev.net/en/categories/media/</link>
    <description>Recent content in Media on LNA-DEV ~ Lukas Nagel</description>
    <image>
      <title>LNA-DEV ~ Lukas Nagel</title>
      <url>https://lna-dev.net/assets/Ping%C3%BCino/Ping%C3%BCino.png</url>
      <link>https://lna-dev.net/assets/Ping%C3%BCino/Ping%C3%BCino.png</link>
    </image>
    <generator>Hugo -- 0.160.1</generator>
    <language>en-US</language>
    <managingEditor>me@lna-dev.net (Lukas Nagel)</managingEditor>
    <webMaster>me@lna-dev.net (Lukas Nagel)</webMaster>
    <copyright>🄯 various licenses</copyright>
    <lastBuildDate>Sat, 10 May 2025 23:10:00 +0200</lastBuildDate>
    <atom:link href="https://lna-dev.net/en/categories/media/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>This website is now on the darknet</title>
      <link>https://lna-dev.net/en/posts/privacy/tor/</link>
      <pubDate>Sat, 10 May 2025 23:10:00 +0200</pubDate><author>me@lna-dev.net (Lukas Nagel)</author>
      <guid>https://lna-dev.net/en/posts/privacy/tor/</guid>
      <description>Why I decided to publish my website on the darknet and a bit of background information about it and the Tor network.</description>
      <content:encoded><![CDATA[<h2 id="tor-as-the-gateway-to-information">Tor as the gateway to information</h2>
<p>The Tor network empowers people to access the uncensored internet from many places around the world. It provides a way for all of humanity to access information in a free and open manner. It includes many tools and options to circumvent censorship and even active measures taken against it. Tor bridges or the Snowflake browser add-on, for example, help people in censored countries connect to Tor even when the known entry node IPs are blocked by the state.</p>
<h2 id="how-tor-gives-you-freedom-of-speech">How Tor gives you freedom of speech</h2>
<p>But that’s only half of it. The freedom to acquire information is arguably one of the most fundamental qualities of a good life. Freedom of expression is also essential for a free and fulfilling life. Tor also enables people who have to fear the consequences of an authoritarian state to publish their opinions as anonymously as possible and therefore protect those freedoms.</p>
<h2 id="how-you-can-use-tor">How you can use Tor</h2>
<p>For those of you not very tech-savvy, here’s a little summary of how Tor works and how you can access the &ldquo;darknet&rdquo;.</p>
<p>If you want to start browsing the Tor network and the &ldquo;normal&rdquo; World Wide Web, you need a special browser. This browser is called the Tor Browser and you can download it <a href="https://www.torproject.org/download/">here</a>. With this browser installed, you can use it like any other browser.</p>
<p>You might notice that it is a bit slow, but this is because of how the Tor network works. Basically, your whole traffic is encrypted multiple times like an onion. That’s also where the name Tor comes from — <strong>T</strong>he <strong>O</strong>nion <strong>R</strong>outer. This traffic is then sent over multiple computers, so your initial location can only be traced back with difficulty and under very specific conditions. This enables the end user to stay private.</p>
<p>There is also a new type of top-level domain available if you are using the Tor browser. (A top-level domain (TLD) is something like <code>.com</code>, <code>.org</code>, <code>.net</code>, <code>.de</code>, <code>.au</code>, <code>.nz</code>, &hellip;) This TLD is <code>.onion</code> and can only be accessed over the Tor network. You can get a domain like that for free and they look kind of weird. Here is mine, for example: <a href="http://lnadevwj2vzomixiunv7i4lahwpoxh6zw56cxbce3uui5ijmwt4czpyd.onion">lnadevwj2vzomixiunv7i4lahwpoxh6zw56cxbce3uui5ijmwt4czpyd.onion</a>. That is because there need to be many different ones if you want to give them out for free.</p>
<p>These <code>.onion</code> domains allow the creator of the website to hide their IP address and therefore stay (sort of) &ldquo;anonymous&rdquo; — similar to the user, but the other way around. This is, like I said before, needed if you want to express yourself from within a repressive country. But even today it is needed for activists, journalists, whistleblowers and many more groups of people needed for a well-functioning and free democracy.</p>
<h2 id="why-does-that-concern-me">Why does that concern me?</h2>
<p>Our world gets more autocratic and dangerous day by day. Looking at you, USA&hellip; But even here in Europe, things look bad. Far-right-wing parties with high vote counts and corrupt politicians, for a start. More worrying is that even the more democratic parties are applying more and more autocratic tools like mass surveillance and strengthening the police&hellip;
Still, the situation in America is way worse — not even speaking of all the other parts of the world like China or Russia, which are the authoritarian powerhouses of this world.</p>
<p>With all this repression, there is a big need in the world for tools like Tor and they need to be supported as much as possible. I wanted to check out how difficult it is to host a website — in my case, this one — on Tor, or as the mainstream calls it, &ldquo;the darknet&rdquo;. As it turns out, it is extremely easy, as you’ll see in one of my upcoming blog posts about how my setup works. But for now, let’s just say you can skip a couple of steps compared to a traditional website. You don’t need to buy a domain — you just get one for free. You also don’t need to worry about any firewalls or anything similar. Tor goes straight through it. Remember, Tor was designed to thrive in a censored environment.</p>
<p>With the knowledge to host a website over the Tor network and the information on how easy it is broadly available, we are better prepared for a reality that isn’t as fine as our current situation (in which many people currently live and suffer under).</p>
<p>So for me, it was an experiment to see how this technology works and to gain the knowledge — and hope I never need it. My website is the same here and on the darknet, so I have no anonymity boost because of it. But after all, it’s also just very cool and interesting to me. Censorship-prevention technologies have fascinated me since like forever.</p>
<h2 id="how-you-can-learn-more-about-this-topic">How you can learn more about this topic</h2>
<p>There are a couple of good sources about the threat of authoritarianism, Tor and how it can help. The most obvious source is the <a href="https://www.torproject.org/">website of Tor</a> itself. There you can find a lot of information about how Tor works and how to use it. Also consider going through the blog posts on their page to see news and that sort of thing.</p>
<p>The Electronic Frontier Foundation (EFF) has a section about <a href="https://www.eff.org/issues/free-speech">free speech</a> and <a href="https://www.eff.org/search/site/tor?f%5B0%5D=type%3Apage">a bit about Tor</a>.</p>
<p>There are many other resources out there — explore, read and stay curious. The more people understand and support tools like these, the more resilient we become.</p>
]]></content:encoded>
    </item>
    <item>
      <title>How to automate Bluesky posts</title>
      <link>https://lna-dev.net/en/posts/projects/bluesky-automation/</link>
      <pubDate>Wed, 29 Jan 2025 18:50:00 +0200</pubDate><author>me@lna-dev.net (Lukas Nagel)</author>
      <guid>https://lna-dev.net/en/posts/projects/bluesky-automation/</guid>
      <description>I recently created my Bluesky account and in this post, I’ll show you how I automate my posts to it.</description>
      <content:encoded><![CDATA[<p>I had Bluesky on my radar for quite a while but never liked the way the platform was built. <sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup> But recently, I decided to give it a try. With this post, I want to share my experience automating my posts to Bluesky with you. (You can find <a href="https://bsky.app/profile/lna-dev.net">my profile</a> here.)</p>
<p>I also wrote <a href="../pixelfed-automation/">a post</a> in which I explained how to automate posts to <a href="../../media/fediverse/#pixelfed">Pixelfed</a> (a <a href="../../media/fediverse/">Fediverse</a> platform), which is quite similar to this one. I might refer to that post from time to time because the process is quite similar.</p>
<blockquote>
<p>This post might be a bit technical if you have no previous experience with coding / tech.</p>
</blockquote>
<h2 id="posse">POSSE</h2>
<p>So, first of all, why do all this? For me, the answer is simple: I have been getting into the <a href="https://indieweb.org/">IndieWeb</a> bubble for a while. As part of it, you want to build your own website, which you control and have full power over. Your website is your little garden where you put all your stuff. But because most people won’t just visit your website directly, you need a way to get your content to them. For this, automation is key.</p>
<p>You host all your content on your page and then build scripts and little bots that take the content from your webpage and <strong>syndicate</strong> it wherever you want. Whether it&rsquo;s the walled gardens of big tech, the <a href="../../media/fediverse/">Fediverse</a> or Bluesky doesn’t matter. You just post to your website and your bots do the rest. This principle is called <em>Post On your Own Site Syndicate Elsewhere</em> or <strong>POSSE</strong> for short.</p>
<p>I might write a detailed post about the IndieWeb and <a href="https://www.citationneeded.news/posse/">POSSE</a> in the future, so stay tuned!</p>
<h2 id="my-data-source---rss-feed">My data source - RSS Feed</h2>
<p>As I already explained in my <a href="../pixelfed-automation/#rss-feed">Pixelfed Automation</a> post, I have an RSS feed that contains all the data I need for publishing my images. Specifically, this includes the image URL, description, alt text and hashtags.</p>
<p>Your data source may vary if you have an API or something similar to access the required data. Personally, I like the idea of having an RSS feed because users of my webpage can follow the same feed and use an RSS reader to consume my content.</p>
<h2 id="the-python-script">The Python script</h2>
<p>Now, onto the actual automation. I used Python because I already had my Pixelfed script, which I could reuse a lot from.</p>
<p>There are a couple of code blocks that I didn’t really touch. For example, the parts responsible for retrieving the image from my website, selecting which image to publish and notifying my API about the images that are now online.</p>
<p>Since I’ve covered these steps before, I’ll jump straight to the interesting part: how to publish to Bluesky. (If you want to know the details about the other steps, you can read them <a href="../pixelfed-automation/#the-pixelfed-script">here</a>.)</p>
<h3 id="publish-to-bluesky">Publish to Bluesky</h3>
<p>This is where it gets interesting because we’re doing the actual publishing. First of all, there is a good Python library for Bluesky / ATproto called <code>atproto</code>. You can install it with the following command:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-sh" data-lang="sh"><span style="display:flex;"><span>pip install atproto
</span></span></code></pre></div><p>After installing the right package, we can look at the code itself. First, we need a client where we pass our email and PAT (I explain how to generate one in the next section). This is enough to authenticate with Bluesky.</p>
<p>Next, we prepare our post. For this, I use the <code>TextBuilder</code> provided by the package to format links and hashtags correctly. After preparing our text/caption, we can use the <code>send_image</code> or <code>send_post</code> method, respectively. And that’s pretty much it! Quite easy when using the library.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#f92672">from</span> atproto <span style="color:#f92672">import</span> Client
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> atproto <span style="color:#f92672">import</span> client_utils
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>BLUESKY_PAT <span style="color:#f92672">=</span> os<span style="color:#f92672">.</span>environ<span style="color:#f92672">.</span>get(<span style="color:#e6db74">&#39;BLUESKY_PAT&#39;</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>bsClient <span style="color:#f92672">=</span> Client()
</span></span><span style="display:flex;"><span>bsClient<span style="color:#f92672">.</span>login(
</span></span><span style="display:flex;"><span>    login<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;bluesky@lna-dev.net&#34;</span>,
</span></span><span style="display:flex;"><span>    password<span style="color:#f92672">=</span>BLUESKY_PAT,
</span></span><span style="display:flex;"><span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># [...]</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">publish_entry</span>(entry):
</span></span><span style="display:flex;"><span>    caption <span style="color:#f92672">=</span> client_utils<span style="color:#f92672">.</span>TextBuilder()
</span></span><span style="display:flex;"><span>    caption<span style="color:#f92672">.</span>text(<span style="color:#e6db74">&#34;More at &#34;</span>)
</span></span><span style="display:flex;"><span>    caption<span style="color:#f92672">.</span>link(text<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;https://lna-dev.net/en/gallery&#34;</span>, url<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;https://lna-dev.net/en/gallery&#34;</span>)
</span></span><span style="display:flex;"><span>    caption<span style="color:#f92672">.</span>text(<span style="color:#e6db74">&#34;</span><span style="color:#ae81ff">\n\n</span><span style="color:#e6db74">&#34;</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">for</span> element <span style="color:#f92672">in</span> entry<span style="color:#f92672">.</span>tags:
</span></span><span style="display:flex;"><span>        caption<span style="color:#f92672">.</span>tag(<span style="color:#e6db74">&#34;#&#34;</span> <span style="color:#f92672">+</span> element<span style="color:#f92672">.</span>term, element<span style="color:#f92672">.</span>term)
</span></span><span style="display:flex;"><span>        caption<span style="color:#f92672">.</span>text(<span style="color:#e6db74">&#34; &#34;</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    media_url <span style="color:#f92672">=</span> entry<span style="color:#f92672">.</span>media_content[<span style="color:#ae81ff">0</span>][<span style="color:#e6db74">&#34;url&#34;</span>]
</span></span><span style="display:flex;"><span>    alt_text <span style="color:#f92672">=</span> re<span style="color:#f92672">.</span>search(<span style="color:#e6db74">&#39;alt=&#34;(.*?)&#34;&#39;</span>, entry<span style="color:#f92672">.</span>summary)
</span></span><span style="display:flex;"><span>    alt_text <span style="color:#f92672">=</span> alt_text<span style="color:#f92672">.</span>group(<span style="color:#ae81ff">1</span>) <span style="color:#66d9ef">if</span> alt_text <span style="color:#66d9ef">else</span> <span style="color:#e6db74">&#34;Alt not found&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    bsClient<span style="color:#f92672">.</span>send_image(
</span></span><span style="display:flex;"><span>        text<span style="color:#f92672">=</span>caption,
</span></span><span style="display:flex;"><span>        image<span style="color:#f92672">=</span>download_image(media_url),
</span></span><span style="display:flex;"><span>        image_alt<span style="color:#f92672">=</span>alt_text,
</span></span><span style="display:flex;"><span>    )
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    published_entry(entry<span style="color:#f92672">.</span>title)
</span></span></code></pre></div><h3 id="how-to-get-your-bluesky-pat">How to get your Bluesky PAT</h3>
<p>To generate a <em>Personal Access Token</em> (<strong>PAT</strong>), also called an <em>App password</em>, go to <code>Settings =&gt; Privacy and Security =&gt; App passwords</code> or click <a href="https://bsky.app/settings/privacy-and-security">here</a>.</p>
<p>Now you can just click <code>Add App Password</code> and copy the text. That’s it! You can now add this to your script or environment variables.</p>
<h3 id="full-code">Full code</h3>


<p><details >
  <summary markdown="span">The full code</summary>
  <p>Alternatively to this code block, there is an up-to-date <a href="https://github.com/LNA-DEV/Autouploader">Repo</a> on GitHub.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#f92672">from</span> io <span style="color:#f92672">import</span> BytesIO
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> os
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> re
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> sys
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> feedparser
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> datetime <span style="color:#f92672">import</span> datetime
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> time
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> random
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> requests
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> atproto <span style="color:#f92672">import</span> Client
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> atproto <span style="color:#f92672">import</span> client_utils
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>BLUESKY_PAT <span style="color:#f92672">=</span> os<span style="color:#f92672">.</span>environ<span style="color:#f92672">.</span>get(<span style="color:#e6db74">&#39;BLUESKY_PAT&#39;</span>)
</span></span><span style="display:flex;"><span>API_KEY <span style="color:#f92672">=</span> os<span style="color:#f92672">.</span>environ<span style="color:#f92672">.</span>get(<span style="color:#e6db74">&#39;API_KEY&#39;</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>bsClient <span style="color:#f92672">=</span> Client()
</span></span><span style="display:flex;"><span>bsClient<span style="color:#f92672">.</span>login(
</span></span><span style="display:flex;"><span>    login<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;bluesky@lna-dev.net&#34;</span>,
</span></span><span style="display:flex;"><span>    password<span style="color:#f92672">=</span>BLUESKY_PAT,
</span></span><span style="display:flex;"><span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Function to filter entries based on the name list</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">filter_entries</span>(entries, name_list):
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e"># Temp skips (for example if this image does not fit currently)</span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e"># name_list.append(&#34;P1002496.JPG&#34;)</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> [entry <span style="color:#66d9ef">for</span> entry <span style="color:#f92672">in</span> entries <span style="color:#66d9ef">if</span> entry<span style="color:#f92672">.</span>title <span style="color:#f92672">not</span> <span style="color:#f92672">in</span> name_list]
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">get_already_uploaded_items</span>():
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">try</span>:
</span></span><span style="display:flex;"><span>        response <span style="color:#f92672">=</span> requests<span style="color:#f92672">.</span>get(<span style="color:#e6db74">&#34;https://api.lna-dev.net/autouploader/bluesky&#34;</span>)
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> response<span style="color:#f92672">.</span>status_code <span style="color:#f92672">==</span> <span style="color:#ae81ff">200</span>:
</span></span><span style="display:flex;"><span>            string_list <span style="color:#f92672">=</span> response<span style="color:#f92672">.</span>json()
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">return</span> string_list
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">else</span>:
</span></span><span style="display:flex;"><span>            print(<span style="color:#e6db74">f</span><span style="color:#e6db74">&#34;Failed to fetch data from API. Status code: </span><span style="color:#e6db74">{</span>response<span style="color:#f92672">.</span>status_code<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span>)
</span></span><span style="display:flex;"><span>            sys<span style="color:#f92672">.</span>exit(<span style="color:#ae81ff">1</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">except</span> <span style="color:#a6e22e">Exception</span> <span style="color:#66d9ef">as</span> e:
</span></span><span style="display:flex;"><span>        print(<span style="color:#e6db74">f</span><span style="color:#e6db74">&#34;An error occurred: </span><span style="color:#e6db74">{</span>e<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span>)
</span></span><span style="display:flex;"><span>        sys<span style="color:#f92672">.</span>exit(<span style="color:#ae81ff">1</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">published_entry</span>(entry_name):
</span></span><span style="display:flex;"><span>    requests<span style="color:#f92672">.</span>post(<span style="color:#e6db74">f</span><span style="color:#e6db74">&#34;https://api.lna-dev.net/autouploader/bluesky?item=</span><span style="color:#e6db74">{</span>entry_name<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span>, headers<span style="color:#f92672">=</span>{<span style="color:#e6db74">&#34;Authorization&#34;</span>: <span style="color:#e6db74">f</span><span style="color:#e6db74">&#34;ApiKey </span><span style="color:#e6db74">{</span>API_KEY<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span>})
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">download_image</span>(image_url):
</span></span><span style="display:flex;"><span>    response <span style="color:#f92672">=</span> requests<span style="color:#f92672">.</span>get(image_url)
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> response<span style="color:#f92672">.</span>status_code <span style="color:#f92672">==</span> <span style="color:#ae81ff">200</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> BytesIO(response<span style="color:#f92672">.</span>content)
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">else</span>:
</span></span><span style="display:flex;"><span>        print(<span style="color:#e6db74">&#34;Failed to download image!&#34;</span>)
</span></span><span style="display:flex;"><span>        sys<span style="color:#f92672">.</span>exit(<span style="color:#ae81ff">1</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">publish_entry</span>(entry):
</span></span><span style="display:flex;"><span>    caption <span style="color:#f92672">=</span> client_utils<span style="color:#f92672">.</span>TextBuilder()
</span></span><span style="display:flex;"><span>    caption<span style="color:#f92672">.</span>text(<span style="color:#e6db74">&#34;More at &#34;</span>)
</span></span><span style="display:flex;"><span>    caption<span style="color:#f92672">.</span>link(text<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;https://lna-dev.net/en/gallery&#34;</span>, url<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;https://lna-dev.net/en/gallery&#34;</span>)
</span></span><span style="display:flex;"><span>    caption<span style="color:#f92672">.</span>text(<span style="color:#e6db74">&#34;</span><span style="color:#ae81ff">\n\n</span><span style="color:#e6db74">&#34;</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">for</span> element <span style="color:#f92672">in</span> entry<span style="color:#f92672">.</span>tags:
</span></span><span style="display:flex;"><span>        caption<span style="color:#f92672">.</span>tag(<span style="color:#e6db74">&#34;#&#34;</span> <span style="color:#f92672">+</span> element<span style="color:#f92672">.</span>term, element<span style="color:#f92672">.</span>term)
</span></span><span style="display:flex;"><span>        caption<span style="color:#f92672">.</span>text(<span style="color:#e6db74">&#34; &#34;</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    media_url <span style="color:#f92672">=</span> entry<span style="color:#f92672">.</span>media_content[<span style="color:#ae81ff">0</span>][<span style="color:#e6db74">&#34;url&#34;</span>]
</span></span><span style="display:flex;"><span>    alt_text <span style="color:#f92672">=</span> re<span style="color:#f92672">.</span>search(<span style="color:#e6db74">&#39;alt=&#34;(.*?)&#34;&#39;</span>, entry<span style="color:#f92672">.</span>summary)
</span></span><span style="display:flex;"><span>    alt_text <span style="color:#f92672">=</span> alt_text<span style="color:#f92672">.</span>group(<span style="color:#ae81ff">1</span>) <span style="color:#66d9ef">if</span> alt_text <span style="color:#66d9ef">else</span> <span style="color:#e6db74">&#34;Alt not found&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    bsClient<span style="color:#f92672">.</span>send_image(
</span></span><span style="display:flex;"><span>        text<span style="color:#f92672">=</span>caption,
</span></span><span style="display:flex;"><span>        image<span style="color:#f92672">=</span>download_image(media_url),
</span></span><span style="display:flex;"><span>        image_alt<span style="color:#f92672">=</span>alt_text,
</span></span><span style="display:flex;"><span>    )
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    published_entry(entry<span style="color:#f92672">.</span>title)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Parse the RSS feed</span>
</span></span><span style="display:flex;"><span>feed_url <span style="color:#f92672">=</span> <span style="color:#e6db74">&#39;https://lna-dev.net/en/gallery/index.xml&#39;</span>
</span></span><span style="display:flex;"><span>feed <span style="color:#f92672">=</span> feedparser<span style="color:#f92672">.</span>parse(feed_url)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Filter out entries with specific names</span>
</span></span><span style="display:flex;"><span>specific_names <span style="color:#f92672">=</span> get_already_uploaded_items()
</span></span><span style="display:flex;"><span>filtered_entries <span style="color:#f92672">=</span> filter_entries(feed<span style="color:#f92672">.</span>entries, specific_names)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">if</span> <span style="color:#f92672">not</span> filtered_entries:
</span></span><span style="display:flex;"><span>    print(<span style="color:#e6db74">&#34;No entries available after filtering.&#34;</span>)
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">else</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#75715e"># Calculate time differences considering only month, day, hour, minute, and second</span>
</span></span><span style="display:flex;"><span>    current_time <span style="color:#f92672">=</span> datetime<span style="color:#f92672">.</span>now()
</span></span><span style="display:flex;"><span>    closest_entry <span style="color:#f92672">=</span> <span style="color:#66d9ef">None</span>
</span></span><span style="display:flex;"><span>    skipped_entries <span style="color:#f92672">=</span> []
</span></span><span style="display:flex;"><span>    min_difference <span style="color:#f92672">=</span> <span style="color:#66d9ef">None</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">for</span> entry <span style="color:#f92672">in</span> filtered_entries:
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> entry<span style="color:#f92672">.</span>published_parsed<span style="color:#f92672">.</span>tm_year <span style="color:#f92672">==</span> <span style="color:#ae81ff">0</span> <span style="color:#f92672">or</span> entry<span style="color:#f92672">.</span>published_parsed<span style="color:#f92672">.</span>tm_year <span style="color:#f92672">==</span> <span style="color:#ae81ff">1</span>:
</span></span><span style="display:flex;"><span>            skipped_entries<span style="color:#f92672">.</span>append(entry)
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">continue</span>  <span style="color:#75715e"># Skip entries with invalid year</span>
</span></span><span style="display:flex;"><span>        temp <span style="color:#f92672">=</span> time<span style="color:#f92672">.</span>mktime(entry<span style="color:#f92672">.</span>published_parsed)
</span></span><span style="display:flex;"><span>        published_time <span style="color:#f92672">=</span> datetime<span style="color:#f92672">.</span>fromtimestamp(temp)
</span></span><span style="display:flex;"><span>        difference <span style="color:#f92672">=</span> abs(current_time<span style="color:#f92672">.</span>replace(year<span style="color:#f92672">=</span>published_time<span style="color:#f92672">.</span>year, tzinfo<span style="color:#f92672">=</span><span style="color:#66d9ef">None</span>) <span style="color:#f92672">-</span> published_time)
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> min_difference <span style="color:#f92672">is</span> <span style="color:#66d9ef">None</span> <span style="color:#f92672">or</span> difference <span style="color:#f92672">&lt;</span> min_difference:
</span></span><span style="display:flex;"><span>            min_difference <span style="color:#f92672">=</span> difference
</span></span><span style="display:flex;"><span>            closest_entry <span style="color:#f92672">=</span> entry
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> closest_entry <span style="color:#f92672">is</span> <span style="color:#66d9ef">None</span>:
</span></span><span style="display:flex;"><span>        print(<span style="color:#e6db74">&#34;No valid entries available after filtering.&#34;</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">else</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#75715e"># Get all entries published at the same time as the closest entry</span>
</span></span><span style="display:flex;"><span>        closest_entries <span style="color:#f92672">=</span> [entry <span style="color:#66d9ef">for</span> entry <span style="color:#f92672">in</span> filtered_entries <span style="color:#66d9ef">if</span> entry<span style="color:#f92672">.</span>published <span style="color:#f92672">==</span> closest_entry<span style="color:#f92672">.</span>published]
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">for</span> element <span style="color:#f92672">in</span> skipped_entries:
</span></span><span style="display:flex;"><span>            closest_entries<span style="color:#f92672">.</span>append(element)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        <span style="color:#75715e"># Select a random entry from the closest entries</span>
</span></span><span style="display:flex;"><span>        random_entry <span style="color:#f92672">=</span> random<span style="color:#f92672">.</span>choice(closest_entries)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        <span style="color:#75715e"># Print the selected entry</span>
</span></span><span style="display:flex;"><span>        print(<span style="color:#e6db74">&#34;Random entry closest to the current date/time (ignoring year):&#34;</span>)
</span></span><span style="display:flex;"><span>        print(<span style="color:#e6db74">&#34;Title:&#34;</span>, random_entry<span style="color:#f92672">.</span>title)
</span></span><span style="display:flex;"><span>        print(<span style="color:#e6db74">&#34;URL:&#34;</span>, random_entry<span style="color:#f92672">.</span>link)
</span></span><span style="display:flex;"><span>        print(<span style="color:#e6db74">&#34;Published Date:&#34;</span>, random_entry<span style="color:#f92672">.</span>published)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        publish_entry(random_entry)
</span></span></code></pre></div>
</details></p>

<h2 id="schedule-the-script">Schedule the script</h2>
<p>After creating such a script, you need to trigger it somehow. If you are using <a href="../../../tags/kubernetes/">Kubernetes</a>, you may want to check out the <a href="../pixelfed-automation/#the-schedule">Pixelfed post</a>, which explains how to set up a Kubernetes CronJob.</p>
<p>For those who just have a simple server, I recommend setting up a Linux CronJob to trigger the script periodically.</p>
<div class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1">
<p>I mainly have two big problems with Bluesky. The first is that there is already a good protocol for decentralized social networks. So why reinvent the wheel? And not only did they reinvent it, but they also made it worse. The AT protocol relies much more on centralized services controlled by Bluesky, which is completely different from <a href="../../../tags/activitypub/">ActivityPub</a>. The second issue is that Bluesky is a for-profit company instead of a nonprofit, which is the standard in the <a href="../../media/fediverse/">Fediverse</a>. What’s really alarming is that this company doesn’t yet know how it wants to make money. So, it’s realistic that the platform will get worse in the future to generate revenue.&#160;<a href="#fnref:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
</ol>
</div>
]]></content:encoded>
    </item>
    <item>
      <title>How to automate Pixelfed posts</title>
      <link>https://lna-dev.net/en/posts/projects/pixelfed-automation/</link>
      <pubDate>Sun, 05 May 2024 22:30:45 +0200</pubDate><author>me@lna-dev.net (Lukas Nagel)</author>
      <guid>https://lna-dev.net/en/posts/projects/pixelfed-automation/</guid>
      <description>I needed to automate my posts to Pixelfed. In this post, I describe how I achieved that.</description>
      <content:encoded><![CDATA[<h2 id="my-problem-with-posting-to-pixelfed">My problem with posting to Pixelfed</h2>
<p>I started with Mastodon as my first account in the <a href="posts/media/fediverse/">Fediverse</a>. After a while, I found out about Pixelfed and created my account there. I kept using it for a while and even posted from time to time. My problem with posting to Pixelfed was the lack of a scheduler. I want to post regularly, but I do not want to login every day and make a post manually. This feature was even announced but never finished. Even today, this is not implemented. There is also a <a href="https://github.com/pixelfed/pixelfed/issues/2872">GitHub issue</a> discussing this if your interest goes deeper. In the following post by Pixelfed, you can see the announcement.


<iframe
  src="https://mastodon.social/@pixelfed/107574719894032457/embed"
  class="mastodon-embed"
  style="max-width: 100%; border: 0; margin-top: 15px; margin-bottom: 10px"
  width="100%"
  allowfullscreen="allowfullscreen"
></iframe>
<script src="https://mastodon.social/embed.js" async="async"></script>
</p>
<p>There are some post schedulers for Mastodon out there. I tried using them with Pixelfed, but nothing worked properly. I even tried the client-side automatic upload feature of <a href="https://fedilab.app/">Fedilab</a>, which posted immediately instead of at the given time. This surely was a bug I could have reported or fixed myself (because Fedilab is <a href="tags/open-source/">FOSS</a>). But I wanted to make things more automatic anyway.</p>
<p>I do not want to complain about this. It is totally fine. There are a lot of other things that need to be done in Pixelfed and the Fediverse as a whole. But for me, it meant I needed to implement something on my own.</p>
<h2 id="requirements">Requirements</h2>
<p>So to solve this, I needed something which can somehow talk with my Pixelfed server and make a post there. I also needed a way to know which posts I want to make. This information must include some metadata like the date taken and definitely an alt text and tags to display in Pixelfed. Another thing to keep in mind is that I needed something which &ldquo;remembers&rdquo; the state of my postings. Basically, a list in which each post gets added if it is uploaded successfully. And finally, I needed some sort of mechanism which can schedule the execution of the code.</p>
<h2 id="the-automation">The automation</h2>
<h3 id="rss-feed">RSS feed</h3>
<p>So first of all, I have a <a href="https://lna-dev.net/en/gallery">photography page</a> which has an RSS feed. I modified the feed of the page to contain an entry per image I have uploaded there. I am also using Hugo there. Therefore I wrote this little Hugo <code>rss.xml</code>.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-xml" data-lang="xml"><span style="display:flex;"><span>{{- $authorEmail := site.Params.author.email -}}
</span></span><span style="display:flex;"><span>{{- $authorName := site.Params.author.name }}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>{{- $pctx := . -}}
</span></span><span style="display:flex;"><span>{{- if .IsHome -}}{{ $pctx = .Site }}{{- end -}}
</span></span><span style="display:flex;"><span>{{- $pages := slice -}}
</span></span><span style="display:flex;"><span>{{- if or $.IsHome $.IsSection -}}
</span></span><span style="display:flex;"><span>{{- $pages = where $pctx.RegularPages &#34;Params.private&#34; &#34;ne&#34; true }}
</span></span><span style="display:flex;"><span>{{- else -}}
</span></span><span style="display:flex;"><span>{{- $pages = where $pctx.Pages &#34;Params.private&#34; &#34;ne&#34; true }}
</span></span><span style="display:flex;"><span>{{- end -}}
</span></span><span style="display:flex;"><span>{{- $pages = where $pages &#34;Params.rss_ignore&#34; &#34;ne&#34; true -}}
</span></span><span style="display:flex;"><span>{{- $limit := .Site.Config.Services.RSS.Limit -}}
</span></span><span style="display:flex;"><span>{{- if ge $limit 1 -}}
</span></span><span style="display:flex;"><span>{{- $pages = $pages | first $limit -}}
</span></span><span style="display:flex;"><span>{{- end -}}
</span></span><span style="display:flex;"><span>{{- printf &#34;<span style="color:#75715e">&lt;?xml version=\&#34;1.0\&#34; encoding=\&#34;utf-8\&#34; standalone=\&#34;yes\&#34;?&gt;</span>&#34; | safeHTML }}
</span></span><span style="display:flex;"><span><span style="color:#f92672">&lt;rss</span> <span style="color:#a6e22e">version=</span><span style="color:#e6db74">&#34;2.0&#34;</span> <span style="color:#a6e22e">xmlns:media=</span><span style="color:#e6db74">&#34;http://search.yahoo.com/mrss/&#34;</span> <span style="color:#a6e22e">xmlns:atom=</span><span style="color:#e6db74">&#34;http://www.w3.org/2005/Atom&#34;</span><span style="color:#f92672">&gt;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&lt;channel&gt;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&lt;title&gt;</span>{{ if eq  .Title  .Site.Title }}{{ .Site.Title }}{{ else }}{{ with .Title }}{{.}} on {{ end }}{{ .Site.Title }}{{ end }}<span style="color:#f92672">&lt;/title&gt;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&lt;link&gt;</span>{{ .Permalink }}<span style="color:#f92672">&lt;/link&gt;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&lt;description&gt;</span>Recent content {{ if ne  .Title  .Site.Title }}{{ with .Title }}in {{.}} {{ end }}{{ end }}on {{ .Site.Title }}<span style="color:#f92672">&lt;/description&gt;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&lt;generator&gt;</span>Hugo -- gohugo.io<span style="color:#f92672">&lt;/generator&gt;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&lt;language&gt;</span>{{ site.Language.LanguageCode }}<span style="color:#f92672">&lt;/language&gt;</span>{{ with site.Params.author.email }}
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&lt;managingEditor&gt;</span>{{.}}{{ with $authorName }} ({{ . }}){{ end }}<span style="color:#f92672">&lt;/managingEditor&gt;</span>{{ end }}{{ with $authorEmail }}
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&lt;webMaster&gt;</span>{{ . }}{{ with $authorName }} ({{ . }}){{ end }}<span style="color:#f92672">&lt;/webMaster&gt;</span>{{ end }}{{ with .Site.Copyright }}
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&lt;copyright&gt;</span>{{.}}<span style="color:#f92672">&lt;/copyright&gt;</span>{{end}}{{ if not .Date.IsZero }}
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&lt;lastBuildDate&gt;</span>{{ (index $pages.ByLastmod.Reverse 0).Lastmod.Format &#34;Mon, 02 Jan 2006 15:04:05 -0700&#34; | safeHTML }}<span style="color:#f92672">&lt;/lastBuildDate&gt;</span>{{ end }}
</span></span><span style="display:flex;"><span>    {{- with .OutputFormats.Get &#34;RSS&#34; -}}
</span></span><span style="display:flex;"><span>    {{ printf &#34;<span style="color:#f92672">&lt;atom:link</span> <span style="color:#a6e22e">href=</span><span style="color:#e6db74">%q</span> <span style="color:#a6e22e">rel=</span><span style="color:#e6db74">\&#34;self\&#34;</span> <span style="color:#a6e22e">type=</span><span style="color:#e6db74">%q</span> <span style="color:#f92672">/&gt;</span>&#34; .Permalink .MediaType | safeHTML }}
</span></span><span style="display:flex;"><span>    {{- end -}}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    {{- range $pages }}
</span></span><span style="display:flex;"><span>    {{- if not (in .Path &#34;/archive/&#34;) -}}
</span></span><span style="display:flex;"><span>    {{ range .Resources.ByType &#34;image&#34; }}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&lt;item&gt;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">&lt;title&gt;</span>{{ .Name }}<span style="color:#f92672">&lt;/title&gt;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">&lt;link&gt;</span>{{ .Permalink }}<span style="color:#f92672">&lt;/link&gt;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">&lt;pubDate&gt;</span>{{ .Exif.Date.Format &#34;Mon, 02 Jan 2006 15:04:05 -0700&#34; | safeHTML }}<span style="color:#f92672">&lt;/pubDate&gt;</span>
</span></span><span style="display:flex;"><span>      {{- with $authorEmail }}<span style="color:#f92672">&lt;author&gt;</span>{{ . }}{{ with $authorName }} ({{ . }}){{ end }}<span style="color:#f92672">&lt;/author&gt;</span>{{ end }}
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">&lt;guid&gt;</span>{{ .Permalink }}<span style="color:#f92672">&lt;/guid&gt;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">&lt;media:content</span> <span style="color:#a6e22e">url=</span><span style="color:#e6db74">&#34;{{ .Permalink }}&#34;</span> <span style="color:#a6e22e">type=</span><span style="color:#e6db74">&#34;image/jpeg&#34;</span><span style="color:#f92672">/&gt;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">&lt;description&gt;</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">&lt;img</span> <span style="color:#a6e22e">src=</span><span style="color:#e6db74">&#34;{{ .Permalink }}&#34;</span> <span style="color:#a6e22e">alt=</span><span style="color:#e6db74">&#34;{{ .Params.Alt }}&#34;</span><span style="color:#f92672">/&gt;</span>          
</span></span><span style="display:flex;"><span>        {{- if ne .Title .Name -}}
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">&lt;p&gt;</span> {{ .Title }} <span style="color:#f92672">&lt;/p&gt;</span>
</span></span><span style="display:flex;"><span>        {{ end }}
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">&lt;/description&gt;</span>
</span></span><span style="display:flex;"><span>      {{ range .Params.Tags }}
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">&lt;category&gt;</span>{{ . }}<span style="color:#f92672">&lt;/category&gt;</span>        
</span></span><span style="display:flex;"><span>      {{- end }}
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&lt;/item&gt;</span>
</span></span><span style="display:flex;"><span>    {{- end -}}
</span></span><span style="display:flex;"><span>    {{- end -}}
</span></span><span style="display:flex;"><span>    {{ end }}
</span></span><span style="display:flex;"><span>    
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&lt;/channel&gt;</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">&lt;/rss&gt;</span>
</span></span></code></pre></div><p>That done, I managed the metadata of all my images in the Hugo config file and wrote an alt text and tags for all my images. (Must admit&hellip; an LLM helped me a bit with that. This made the process a lot faster, but the quality is also just okay.)</p>
<h3 id="my-personal-api">My personal API</h3>
<p>I even wanted to create an API for this page before this little project. So I created a new API written in <strong>go</strong>. There I implemented a few endpoints which can save and read a list of filenames from and to MongoDB. I am using MongoDB because of the easy use and their free cloud storage. I will probably change that to something self-hosted in the future, but for now, it does its job. (I am running a K8s cluster with broken local storage right now. Need to fix this first.)<br>
Oh, and I also made the <code>POST</code> endpoint protected by an API key so no one unauthorized can alter my data😉</p>
<p>Now having an API which knows which images have already been posted, I could continue to the script interacting with Pixelfed.</p>
<h3 id="the-pixelfed-script">The Pixelfed Script</h3>
<p>For writing the script, I went with Python. I am having mixed feelings about Python. On the one hand, I really dislike the way it is handling types (it doesn&rsquo;t lol) but on the other hand, you can quickly make basic things work. So I went the (for me) experimental way and chose Python as my way to go.</p>
<p>I created this basic script which is handling all the stuff needed to create the post and make all the API and RSS handling. I go into detail on some of the methods and bits of code I find most important but I have the full code provided below if you are interested.</p>


<p><details >
  <summary markdown="span">The full code</summary>
  <p>Alternatively to this code block, there is an up-to-date <a href="https://github.com/LNA-DEV/Autouploader">Repo</a> on GitHub.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#f92672">from</span> io <span style="color:#f92672">import</span> BytesIO
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> os
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> re
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> sys
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> feedparser
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> datetime <span style="color:#f92672">import</span> datetime
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> time
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> random
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> requests
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>PIXELFED_INSTANCE_URL <span style="color:#f92672">=</span> <span style="color:#e6db74">&#39;https://pixelfed.de&#39;</span>
</span></span><span style="display:flex;"><span>PAT <span style="color:#f92672">=</span> os<span style="color:#f92672">.</span>environ<span style="color:#f92672">.</span>get(<span style="color:#e6db74">&#39;PIXELFED_PAT&#39;</span>)
</span></span><span style="display:flex;"><span>API_KEY <span style="color:#f92672">=</span> os<span style="color:#f92672">.</span>environ<span style="color:#f92672">.</span>get(<span style="color:#e6db74">&#39;API_KEY&#39;</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Function to filter entries based on the name list</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">filter_entries</span>(entries, name_list):
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e"># Temp skips (for example if this image does not fit currently)</span>
</span></span><span style="display:flex;"><span>    name_list<span style="color:#f92672">.</span>append(<span style="color:#e6db74">&#34;P1002496.JPG&#34;</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> [entry <span style="color:#66d9ef">for</span> entry <span style="color:#f92672">in</span> entries <span style="color:#66d9ef">if</span> entry<span style="color:#f92672">.</span>title <span style="color:#f92672">not</span> <span style="color:#f92672">in</span> name_list]
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">get_already_uploaded_items</span>():
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">try</span>:
</span></span><span style="display:flex;"><span>        response <span style="color:#f92672">=</span> requests<span style="color:#f92672">.</span>get(<span style="color:#e6db74">&#34;https://api.lna-dev.net/autouploader/pixelfed&#34;</span>)
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> response<span style="color:#f92672">.</span>status_code <span style="color:#f92672">==</span> <span style="color:#ae81ff">200</span>:
</span></span><span style="display:flex;"><span>            string_list <span style="color:#f92672">=</span> response<span style="color:#f92672">.</span>json()
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">return</span> string_list
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">else</span>:
</span></span><span style="display:flex;"><span>            print(<span style="color:#e6db74">f</span><span style="color:#e6db74">&#34;Failed to fetch data from API. Status code: </span><span style="color:#e6db74">{</span>response<span style="color:#f92672">.</span>status_code<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span>)
</span></span><span style="display:flex;"><span>            sys<span style="color:#f92672">.</span>exit(<span style="color:#ae81ff">1</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">except</span> <span style="color:#a6e22e">Exception</span> <span style="color:#66d9ef">as</span> e:
</span></span><span style="display:flex;"><span>        print(<span style="color:#e6db74">f</span><span style="color:#e6db74">&#34;An error occurred: </span><span style="color:#e6db74">{</span>e<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span>)
</span></span><span style="display:flex;"><span>        sys<span style="color:#f92672">.</span>exit(<span style="color:#ae81ff">1</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">published_entry</span>(entry_name):
</span></span><span style="display:flex;"><span>    requests<span style="color:#f92672">.</span>post(<span style="color:#e6db74">f</span><span style="color:#e6db74">&#34;https://api.lna-dev.net/autouploader/pixelfed?item=</span><span style="color:#e6db74">{</span>entry_name<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span>, headers<span style="color:#f92672">=</span>{<span style="color:#e6db74">&#34;Authorization&#34;</span>: <span style="color:#e6db74">f</span><span style="color:#e6db74">&#34;ApiKey </span><span style="color:#e6db74">{</span>API_KEY<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span>})
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">download_image</span>(image_url):
</span></span><span style="display:flex;"><span>    response <span style="color:#f92672">=</span> requests<span style="color:#f92672">.</span>get(image_url)
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> response<span style="color:#f92672">.</span>status_code <span style="color:#f92672">==</span> <span style="color:#ae81ff">200</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> BytesIO(response<span style="color:#f92672">.</span>content)
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">else</span>:
</span></span><span style="display:flex;"><span>        print(<span style="color:#e6db74">&#34;Failed to download image!&#34;</span>)
</span></span><span style="display:flex;"><span>        sys<span style="color:#f92672">.</span>exit(<span style="color:#ae81ff">1</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">publish_entry</span>(entry):
</span></span><span style="display:flex;"><span>    caption <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;More at https://lna-dev.net/en/gallery</span><span style="color:#ae81ff">\n\n</span><span style="color:#e6db74">&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">for</span> element <span style="color:#f92672">in</span> entry<span style="color:#f92672">.</span>tags:
</span></span><span style="display:flex;"><span>        caption <span style="color:#f92672">+=</span> <span style="color:#e6db74">&#39;#&#39;</span> <span style="color:#f92672">+</span> element<span style="color:#f92672">.</span>term <span style="color:#f92672">+</span> <span style="color:#e6db74">&#34; &#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    mediaResponse <span style="color:#f92672">=</span> upload_media(entry)
</span></span><span style="display:flex;"><span>    publish_post(caption, mediaResponse)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    published_entry(entry<span style="color:#f92672">.</span>title)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">upload_media</span>(entry):
</span></span><span style="display:flex;"><span>    media_url <span style="color:#f92672">=</span> <span style="color:#e6db74">f</span><span style="color:#e6db74">&#39;</span><span style="color:#e6db74">{</span>PIXELFED_INSTANCE_URL<span style="color:#e6db74">}</span><span style="color:#e6db74">/api/v1/media&#39;</span>
</span></span><span style="display:flex;"><span>    headers <span style="color:#f92672">=</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#39;Authorization&#39;</span>: <span style="color:#e6db74">f</span><span style="color:#e6db74">&#39;Bearer </span><span style="color:#e6db74">{</span>PAT<span style="color:#e6db74">}</span><span style="color:#e6db74">&#39;</span>,
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#39;Accept&#39;</span>: <span style="color:#e6db74">&#39;application/json&#39;</span>
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>    files <span style="color:#f92672">=</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#39;file&#39;</span>: download_image(entry<span style="color:#f92672">.</span>link)
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>    data <span style="color:#f92672">=</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#39;description&#39;</span>: re<span style="color:#f92672">.</span>search(<span style="color:#e6db74">&#39;alt=&#34;(.*?)&#34;&#39;</span>, entry<span style="color:#f92672">.</span>summary)<span style="color:#f92672">.</span>group(<span style="color:#ae81ff">1</span>)
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>    response <span style="color:#f92672">=</span> requests<span style="color:#f92672">.</span>post(media_url, headers<span style="color:#f92672">=</span>headers, files<span style="color:#f92672">=</span>files, data<span style="color:#f92672">=</span>data)
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> response<span style="color:#f92672">.</span>status_code <span style="color:#f92672">==</span> <span style="color:#ae81ff">200</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> response<span style="color:#f92672">.</span>json()[<span style="color:#e6db74">&#39;id&#39;</span>]
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">else</span>:
</span></span><span style="display:flex;"><span>        print(<span style="color:#e6db74">&#34;Failed to upload media.&#34;</span>)
</span></span><span style="display:flex;"><span>        sys<span style="color:#f92672">.</span>exit(<span style="color:#ae81ff">1</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">publish_post</span>(caption, media_id):
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> caption<span style="color:#f92672">.</span>strip():
</span></span><span style="display:flex;"><span>        post_url <span style="color:#f92672">=</span> <span style="color:#e6db74">f</span><span style="color:#e6db74">&#39;</span><span style="color:#e6db74">{</span>PIXELFED_INSTANCE_URL<span style="color:#e6db74">}</span><span style="color:#e6db74">/api/v1/statuses&#39;</span>
</span></span><span style="display:flex;"><span>        headers <span style="color:#f92672">=</span> {
</span></span><span style="display:flex;"><span>            <span style="color:#e6db74">&#39;Authorization&#39;</span>: <span style="color:#e6db74">f</span><span style="color:#e6db74">&#39;Bearer </span><span style="color:#e6db74">{</span>PAT<span style="color:#e6db74">}</span><span style="color:#e6db74">&#39;</span>,
</span></span><span style="display:flex;"><span>            <span style="color:#e6db74">&#39;Accept&#39;</span>: <span style="color:#e6db74">&#39;application/json&#39;</span>
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>        data <span style="color:#f92672">=</span> {
</span></span><span style="display:flex;"><span>            <span style="color:#e6db74">&#39;status&#39;</span>: caption,
</span></span><span style="display:flex;"><span>            <span style="color:#e6db74">&#39;media_ids[]&#39;</span>: media_id
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>        response <span style="color:#f92672">=</span> requests<span style="color:#f92672">.</span>post(post_url, headers<span style="color:#f92672">=</span>headers, data<span style="color:#f92672">=</span>data)
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> response<span style="color:#f92672">.</span>status_code <span style="color:#f92672">==</span> <span style="color:#ae81ff">200</span>:
</span></span><span style="display:flex;"><span>            print(<span style="color:#e6db74">&#34;Post published successfully!&#34;</span>)
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">else</span>:
</span></span><span style="display:flex;"><span>            print(<span style="color:#e6db74">&#34;Failed to publish post!&#34;</span>)
</span></span><span style="display:flex;"><span>            sys<span style="color:#f92672">.</span>exit(<span style="color:#ae81ff">1</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">else</span>:
</span></span><span style="display:flex;"><span>        print(<span style="color:#e6db74">&#34;Caption cannot be empty.&#34;</span>)
</span></span><span style="display:flex;"><span>        sys<span style="color:#f92672">.</span>exit(<span style="color:#ae81ff">1</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Parse the RSS feed</span>
</span></span><span style="display:flex;"><span>feed_url <span style="color:#f92672">=</span> <span style="color:#e6db74">&#39;https://lna-dev.net/en/gallery/index.xml&#39;</span>
</span></span><span style="display:flex;"><span>feed <span style="color:#f92672">=</span> feedparser<span style="color:#f92672">.</span>parse(feed_url)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Filter out entries with specific names</span>
</span></span><span style="display:flex;"><span>specific_names <span style="color:#f92672">=</span> get_already_uploaded_items()
</span></span><span style="display:flex;"><span>filtered_entries <span style="color:#f92672">=</span> filter_entries(feed<span style="color:#f92672">.</span>entries, specific_names)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">if</span> <span style="color:#f92672">not</span> filtered_entries:
</span></span><span style="display:flex;"><span>    print(<span style="color:#e6db74">&#34;No entries available after filtering.&#34;</span>)
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">else</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#75715e"># Calculate time differences considering only month, day, hour, minute, and second</span>
</span></span><span style="display:flex;"><span>    current_time <span style="color:#f92672">=</span> datetime<span style="color:#f92672">.</span>now()
</span></span><span style="display:flex;"><span>    closest_entry <span style="color:#f92672">=</span> <span style="color:#66d9ef">None</span>
</span></span><span style="display:flex;"><span>    skipped_entries <span style="color:#f92672">=</span> []
</span></span><span style="display:flex;"><span>    min_difference <span style="color:#f92672">=</span> <span style="color:#66d9ef">None</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">for</span> entry <span style="color:#f92672">in</span> filtered_entries:
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> entry<span style="color:#f92672">.</span>published_parsed<span style="color:#f92672">.</span>tm_year <span style="color:#f92672">==</span> <span style="color:#ae81ff">0</span> <span style="color:#f92672">or</span> entry<span style="color:#f92672">.</span>published_parsed<span style="color:#f92672">.</span>tm_year <span style="color:#f92672">==</span> <span style="color:#ae81ff">1</span>:
</span></span><span style="display:flex;"><span>            skipped_entries<span style="color:#f92672">.</span>append(entry)
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">continue</span>  <span style="color:#75715e"># Skip entries with invalid year</span>
</span></span><span style="display:flex;"><span>        temp <span style="color:#f92672">=</span> time<span style="color:#f92672">.</span>mktime(entry<span style="color:#f92672">.</span>published_parsed)
</span></span><span style="display:flex;"><span>        published_time <span style="color:#f92672">=</span> datetime<span style="color:#f92672">.</span>fromtimestamp(temp)
</span></span><span style="display:flex;"><span>        difference <span style="color:#f92672">=</span> abs(current_time<span style="color:#f92672">.</span>replace(year<span style="color:#f92672">=</span>published_time<span style="color:#f92672">.</span>year, tzinfo<span style="color:#f92672">=</span><span style="color:#66d9ef">None</span>) <span style="color:#f92672">-</span> published_time)
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> min_difference <span style="color:#f92672">is</span> <span style="color:#66d9ef">None</span> <span style="color:#f92672">or</span> difference <span style="color:#f92672">&lt;</span> min_difference:
</span></span><span style="display:flex;"><span>            min_difference <span style="color:#f92672">=</span> difference
</span></span><span style="display:flex;"><span>            closest_entry <span style="color:#f92672">=</span> entry
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> closest_entry <span style="color:#f92672">is</span> <span style="color:#66d9ef">None</span>:
</span></span><span style="display:flex;"><span>        print(<span style="color:#e6db74">&#34;No valid entries available after filtering.&#34;</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">else</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#75715e"># Get all entries published at the same time as the closest entry</span>
</span></span><span style="display:flex;"><span>        closest_entries <span style="color:#f92672">=</span> [entry <span style="color:#66d9ef">for</span> entry <span style="color:#f92672">in</span> filtered_entries <span style="color:#66d9ef">if</span> entry<span style="color:#f92672">.</span>published <span style="color:#f92672">==</span> closest_entry<span style="color:#f92672">.</span>published]
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">for</span> element <span style="color:#f92672">in</span> skipped_entries:
</span></span><span style="display:flex;"><span>            closest_entries<span style="color:#f92672">.</span>append(element)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        <span style="color:#75715e"># Select a random entry from the closest entries</span>
</span></span><span style="display:flex;"><span>        random_entry <span style="color:#f92672">=</span> random<span style="color:#f92672">.</span>choice(closest_entries)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        <span style="color:#75715e"># Print the selected entry</span>
</span></span><span style="display:flex;"><span>        print(<span style="color:#e6db74">&#34;Random entry closest to the current date/time (ignoring year):&#34;</span>)
</span></span><span style="display:flex;"><span>        print(<span style="color:#e6db74">&#34;Title:&#34;</span>, random_entry<span style="color:#f92672">.</span>title)
</span></span><span style="display:flex;"><span>        print(<span style="color:#e6db74">&#34;URL:&#34;</span>, random_entry<span style="color:#f92672">.</span>link)
</span></span><span style="display:flex;"><span>        print(<span style="color:#e6db74">&#34;Published Date:&#34;</span>, random_entry<span style="color:#f92672">.</span>published)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        publish_entry(random_entry)
</span></span></code></pre></div>
</details></p>

<p>So let&rsquo;s start with the retrieval of the RSS data. To simplify this for me, I used the <code>feedparser</code> library here. With that, I have access to all the data of the RSS feed in a better-structured way.</p>
<p>With this simple API call, I retrieve the already uploaded posts from my personal API I mentioned above. With this data, I can filter out any entries in the RSS feed which I don&rsquo;t need to upload anymore.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">get_already_uploaded_items</span>():
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">try</span>:
</span></span><span style="display:flex;"><span>        response <span style="color:#f92672">=</span> requests<span style="color:#f92672">.</span>get(<span style="color:#e6db74">&#34;https://api.lna-dev.net/autouploader/pixelfed&#34;</span>)
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> response<span style="color:#f92672">.</span>status_code <span style="color:#f92672">==</span> <span style="color:#ae81ff">200</span>:
</span></span><span style="display:flex;"><span>            string_list <span style="color:#f92672">=</span> response<span style="color:#f92672">.</span>json()
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">return</span> string_list
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">else</span>:
</span></span><span style="display:flex;"><span>            print(<span style="color:#e6db74">f</span><span style="color:#e6db74">&#34;Failed to fetch data from API. Status code: </span><span style="color:#e6db74">{</span>response<span style="color:#f92672">.</span>status_code<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span>)
</span></span><span style="display:flex;"><span>            sys<span style="color:#f92672">.</span>exit(<span style="color:#ae81ff">1</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">except</span> <span style="color:#a6e22e">Exception</span> <span style="color:#66d9ef">as</span> e:
</span></span><span style="display:flex;"><span>        print(<span style="color:#e6db74">f</span><span style="color:#e6db74">&#34;An error occurred: </span><span style="color:#e6db74">{</span>e<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span>)
</span></span><span style="display:flex;"><span>        sys<span style="color:#f92672">.</span>exit(<span style="color:#ae81ff">1</span>)
</span></span></code></pre></div><p>After having a list of possible posts, I need the script to decide which post to post next. For that, I am calculating which posts are nearest to the current day and month without taking the year into the calculation.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#66d9ef">for</span> entry <span style="color:#f92672">in</span> filtered_entries:
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> entry<span style="color:#f92672">.</span>published_parsed<span style="color:#f92672">.</span>tm_year <span style="color:#f92672">==</span> <span style="color:#ae81ff">0</span> <span style="color:#f92672">or</span> entry<span style="color:#f92672">.</span>published_parsed<span style="color:#f92672">.</span>tm_year <span style="color:#f92672">==</span> <span style="color:#ae81ff">1</span>:
</span></span><span style="display:flex;"><span>        skipped_entries<span style="color:#f92672">.</span>append(entry)
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">continue</span>  <span style="color:#75715e"># Skip entries with invalid year</span>
</span></span><span style="display:flex;"><span>    temp <span style="color:#f92672">=</span> time<span style="color:#f92672">.</span>mktime(entry<span style="color:#f92672">.</span>published_parsed)
</span></span><span style="display:flex;"><span>    published_time <span style="color:#f92672">=</span> datetime<span style="color:#f92672">.</span>fromtimestamp(temp)
</span></span><span style="display:flex;"><span>    difference <span style="color:#f92672">=</span> abs(current_time<span style="color:#f92672">.</span>replace(year<span style="color:#f92672">=</span>published_time<span style="color:#f92672">.</span>year, tzinfo<span style="color:#f92672">=</span><span style="color:#66d9ef">None</span>) <span style="color:#f92672">-</span> published_time)
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> min_difference <span style="color:#f92672">is</span> <span style="color:#66d9ef">None</span> <span style="color:#f92672">or</span> difference <span style="color:#f92672">&lt;</span> min_difference:
</span></span><span style="display:flex;"><span>        min_difference <span style="color:#f92672">=</span> difference
</span></span><span style="display:flex;"><span>        closest_entry <span style="color:#f92672">=</span> entry
</span></span></code></pre></div><p>Then I select one of the closest entries randomly. I need to do this because if I would have taken many photos on one day there could be multiple items close to the current date.</p>
<p>Now we are ready for creating the post. Therefore, I am now preparing the caption which is included in the post.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">publish_entry</span>(entry):
</span></span><span style="display:flex;"><span>    caption <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;More at https://lna-dev.net/en/gallery</span><span style="color:#ae81ff">\n\n</span><span style="color:#e6db74">&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">for</span> element <span style="color:#f92672">in</span> entry<span style="color:#f92672">.</span>tags:
</span></span><span style="display:flex;"><span>        caption <span style="color:#f92672">+=</span> <span style="color:#e6db74">&#39;#&#39;</span> <span style="color:#f92672">+</span> element<span style="color:#f92672">.</span>term <span style="color:#f92672">+</span> <span style="color:#e6db74">&#34; &#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    mediaResponse <span style="color:#f92672">=</span> upload_media(entry)
</span></span><span style="display:flex;"><span>    publish_post(caption, mediaResponse)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    published_entry(entry<span style="color:#f92672">.</span>title)
</span></span></code></pre></div><p>Now to the trickiest bit: The actual posting. This was especially interesting because at the time I created this script there was a lack of documentation and I needed to somehow figure this out on my own. I also didn&rsquo;t find many examples searching the internet. The best I could do was look into the Mastodon documentation (because Pixelfeds API is similar) and figure it out on my own.</p>
<p>There are two important things here. First, you have to upload the media first before you can make the post which then simply contains the id of the media you uploaded beforehand. And second, you need to create a PAT (personal access token) and provide it via the bearer syntax to the API.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">upload_media</span>(entry):
</span></span><span style="display:flex;"><span>    media_url <span style="color:#f92672">=</span> <span style="color:#e6db74">f</span><span style="color:#e6db74">&#39;</span><span style="color:#e6db74">{</span>PIXELFED_INSTANCE_URL<span style="color:#e6db74">}</span><span style="color:#e6db74">/api/v1/media&#39;</span>
</span></span><span style="display:flex;"><span>    headers <span style="color:#f92672">=</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#39;Authorization&#39;</span>: <span style="color:#e6db74">f</span><span style="color:#e6db74">&#39;Bearer </span><span style="color:#e6db74">{</span>PAT<span style="color:#e6db74">}</span><span style="color:#e6db74">&#39;</span>,
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#39;Accept&#39;</span>: <span style="color:#e6db74">&#39;application/json&#39;</span>
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>    files <span style="color:#f92672">=</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#39;file&#39;</span>: download_image(entry<span style="color:#f92672">.</span>link)
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>    data <span style="color:#f92672">=</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#39;description&#39;</span>: re<span style="color:#f92672">.</span>search(<span style="color:#e6db74">&#39;alt=&#34;(.*?)&#34;&#39;</span>, entry<span style="color:#f92672">.</span>summary)<span style="color:#f92672">.</span>group(<span style="color:#ae81ff">1</span>)
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>    response <span style="color:#f92672">=</span> requests<span style="color:#f92672">.</span>post(media_url, headers<span style="color:#f92672">=</span>headers, files<span style="color:#f92672">=</span>files, data<span style="color:#f92672">=</span>data)
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> response<span style="color:#f92672">.</span>status_code <span style="color:#f92672">==</span> <span style="color:#ae81ff">200</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> response<span style="color:#f92672">.</span>json()[<span style="color:#e6db74">&#39;id&#39;</span>]
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">else</span>:
</span></span><span style="display:flex;"><span>        print(<span style="color:#e6db74">&#34;Failed to upload media.&#34;</span>)
</span></span><span style="display:flex;"><span>        sys<span style="color:#f92672">.</span>exit(<span style="color:#ae81ff">1</span>)
</span></span></code></pre></div><div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">publish_post</span>(caption, media_id):
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> caption<span style="color:#f92672">.</span>strip():
</span></span><span style="display:flex;"><span>        post_url <span style="color:#f92672">=</span> <span style="color:#e6db74">f</span><span style="color:#e6db74">&#39;</span><span style="color:#e6db74">{</span>PIXELFED_INSTANCE_URL<span style="color:#e6db74">}</span><span style="color:#e6db74">/api/v1/statuses&#39;</span>
</span></span><span style="display:flex;"><span>        headers <span style="color:#f92672">=</span> {
</span></span><span style="display:flex;"><span>            <span style="color:#e6db74">&#39;Authorization&#39;</span>: <span style="color:#e6db74">f</span><span style="color:#e6db74">&#39;Bearer </span><span style="color:#e6db74">{</span>PAT<span style="color:#e6db74">}</span><span style="color:#e6db74">&#39;</span>,
</span></span><span style="display:flex;"><span>            <span style="color:#e6db74">&#39;Accept&#39;</span>: <span style="color:#e6db74">&#39;application/json&#39;</span>
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>        data <span style="color:#f92672">=</span> {
</span></span><span style="display:flex;"><span>            <span style="color:#e6db74">&#39;status&#39;</span>: caption,
</span></span><span style="display:flex;"><span>            <span style="color:#e6db74">&#39;media_ids[]&#39;</span>: media_id
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>        response <span style="color:#f92672">=</span> requests<span style="color:#f92672">.</span>post(post_url, headers<span style="color:#f92672">=</span>headers, data<span style="color:#f92672">=</span>data)
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> response<span style="color:#f92672">.</span>status_code <span style="color:#f92672">==</span> <span style="color:#ae81ff">200</span>:
</span></span><span style="display:flex;"><span>            print(<span style="color:#e6db74">&#34;Post published successfully!&#34;</span>)
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">else</span>:
</span></span><span style="display:flex;"><span>            print(<span style="color:#e6db74">&#34;Failed to publish post!&#34;</span>)
</span></span><span style="display:flex;"><span>            sys<span style="color:#f92672">.</span>exit(<span style="color:#ae81ff">1</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">else</span>:
</span></span><span style="display:flex;"><span>        print(<span style="color:#e6db74">&#34;Caption cannot be empty.&#34;</span>)
</span></span><span style="display:flex;"><span>        sys<span style="color:#f92672">.</span>exit(<span style="color:#ae81ff">1</span>)
</span></span></code></pre></div><p>That done, I made a final API request to my personal API noting that I uploaded the image.</p>
<h3 id="the-schedule">The schedule</h3>
<p>This part was fairly easy because I was already using a <a href="/tags/Kubernetes">Kubernetes</a> cluster. I just needed to create a cronjob running each day and that&rsquo;s it.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">apiVersion</span>: <span style="color:#ae81ff">batch/v1</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">kind</span>: <span style="color:#ae81ff">CronJob</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">metadata</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">name</span>: <span style="color:#ae81ff">pixelfed-autoupload</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">spec</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">schedule</span>: <span style="color:#e6db74">&#34;0 16 * * *&#34;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">jobTemplate</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">spec</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">template</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">spec</span>:
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">containers</span>:
</span></span><span style="display:flex;"><span>            - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">pixelfed-autoupload</span>
</span></span><span style="display:flex;"><span>              <span style="color:#f92672">image</span>: <span style="color:#ae81ff">lnadev/pixelfed-autoupload:{{ .Values.autoupload.pixelfed.version }}</span>
</span></span><span style="display:flex;"><span>              <span style="color:#f92672">imagePullPolicy</span>: <span style="color:#ae81ff">Always</span>
</span></span><span style="display:flex;"><span>              <span style="color:#f92672">resources</span>:
</span></span><span style="display:flex;"><span>                <span style="color:#f92672">limits</span>:
</span></span><span style="display:flex;"><span>                  <span style="color:#f92672">memory</span>: <span style="color:#e6db74">&#34;128Mi&#34;</span> 
</span></span><span style="display:flex;"><span>                  <span style="color:#f92672">cpu</span>: <span style="color:#e6db74">&#34;500m&#34;</span> <span style="color:#75715e"># Don&#39;t use CPU limits in K8s but that is another topic...</span>
</span></span><span style="display:flex;"><span>              <span style="color:#f92672">env</span>:
</span></span><span style="display:flex;"><span>                - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">PIXELFED_PAT</span>
</span></span><span style="display:flex;"><span>                  <span style="color:#f92672">valueFrom</span>:
</span></span><span style="display:flex;"><span>                    <span style="color:#f92672">secretKeyRef</span>:
</span></span><span style="display:flex;"><span>                      <span style="color:#f92672">name</span>: <span style="color:#ae81ff">pixelfed</span>
</span></span><span style="display:flex;"><span>                      <span style="color:#f92672">key</span>: <span style="color:#ae81ff">pat</span>
</span></span><span style="display:flex;"><span>                - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">API_KEY</span>
</span></span><span style="display:flex;"><span>                  <span style="color:#f92672">valueFrom</span>:
</span></span><span style="display:flex;"><span>                    <span style="color:#f92672">secretKeyRef</span>:
</span></span><span style="display:flex;"><span>                      <span style="color:#f92672">name</span>: <span style="color:#ae81ff">personal-api-secret</span>
</span></span><span style="display:flex;"><span>                      <span style="color:#f92672">key</span>: <span style="color:#ae81ff">apikey</span>
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">restartPolicy</span>: <span style="color:#ae81ff">Never</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">backoffLimit</span>: <span style="color:#ae81ff">0</span>
</span></span></code></pre></div><p>One thing I had to learn here is the following: Setting the <code>restartPolicy</code> to <code>never</code> will not prevent the cronjob from retrying. You need to also set the <code>backoffLimit</code> to <code>0</code> to achieve the desired behavior. (This is because those two settings change different retry mechanisms you both want to disable.)</p>
]]></content:encoded>
    </item>
    <item>
      <title>What is the Fediverse?</title>
      <link>https://lna-dev.net/en/posts/media/fediverse/</link>
      <pubDate>Sat, 27 Jan 2024 20:45:02 +0100</pubDate><author>me@lna-dev.net (Lukas Nagel)</author>
      <guid>https://lna-dev.net/en/posts/media/fediverse/</guid>
      <description>Deep dive into the really social network.</description>
      <content:encoded><![CDATA[<figure class="img-right">
    <img loading="lazy" src="./fediverse.svg"
         alt="The Fediverse-Logo"/> 
</figure>

<p>In this post, I want to explain what the Fediverse is and why you should join it. I talk very little about the technical stuff, but this post is intended to be understood by anyone.</p>
<p>Another post for the really technical topics is planned for the future.</p>
<h2 id="where-does-the-name-come-from">Where does the name come from?</h2>
<p>The name is a portmanteau of federation and universe. So the Fediverse is a federated universe.</p>
<p>This name does represent it pretty well. The Fediverse is decentralized web of social media platforms all connected to each other. Anyone can connect with anybody else, even if they are using completely different services. You can imagine it like email. Every user has an email address by a specific service, but they can write with anyone they want, not just with users of the same service.</p>
<p>For those who are familiar with the classic social networks: Imagine using Twitter and seeing Instagram or Facebook posts in your timeline, just like they would all be using Twitter. But that&rsquo;s not all. There also could be YouTube videos or anything else. You decide. That is the beauty of the Fediverse.</p>
<h2 id="why-is-the-fediverse-so-important">Why is the Fediverse so important?</h2>
<h3 id="decentralization">Decentralization</h3>
<p>The old social media platforms are owned by companies. Not so is the Fediverse. First of all, there are many services in the Fediverse, so the whole thing cannot be owned. Just like the internet. Therefore, nobody can force users into something or exploit their market dominance, as the current big players do.</p>
<p>The creators of the Fediverse platform Peertube explained it in a straightforward way. Here is their explanation. (This video is by the way hosted on Peertube)</p>
<div style="position: relative; padding-bottom: 56.25%; margin-bottom: 1rem; height: 0; overflow: hidden;">
<iframe sandbox="allow-same-origin allow-scripts allow-popups" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; border:0;"
    src="https://framatube.org/videos/embed/9dRFC6Ya11NCVeYKn8ZhiD?title=0&warningTitle=0&peertubeLink=0" title="Peertube Video" allowfullscreen>
</iframe>
</div>
<h3 id="free-and-open-source">Free and Open Source</h3>
<p>But even the producers of Fediverse software are very different. It mainly consists of FOSS (Free and Open Source Software), which means the software is genuinely free and independent developers can modify it and help to improve it. FOSS is incredibly important and I will cover its numerous advantages in a future post.</p>
<p>Because of this freedom, making money is more challenging. However, the user doesn&rsquo;t get treated like trash, as the user contributes to the Fediverse through donations. Also, there are no ads because of that.</p>
<p>So, the software producers of the Fediverse are primarily volunteers or non-profit organizations. Both aim to make the internet a better place and not just want to make a bug.</p>
<p>Moreover, it&rsquo;s not just the software producers who act fairly, the people who run the servers are mainly volunteers, organizations, or even states!</p>
<h3 id="the-force-of-the-masses">The force of the masses</h3>
<p>Because you can freely choose your service in the Fediverse, no one gets excluded from society anymore. I find this to be a significant issue with classical social media platforms. I am not using any of these because of all the bad things they do. As a result, I sometimes feel a bit excluded in specific contexts.</p>
<p>This is extremely important when it comes to messengers. Not using the specific service that is popular in your region can have a significant impact. Personally, I stopped using WhatsApp for around a year. In Germany, where I&rsquo;m from, it is used nearly without exception. A few people have <a href="https://www.signal.org/">Signal</a> or <a href="https://threema.ch/en">Threema</a>, but that&rsquo;s about it. Not having WhatsApp did affect my day-to-day life significantly. While I thought that using other services might encourage a few people to switch, it did only to some extent. Not everyone made the transition and some friends or family members remained exclusively on WhatsApp. Beyond the challenge of communicating with those you know, like and love, there are difficulties in meeting new people. I&rsquo;ve had experiences where some form negative opinions if you don&rsquo;t use the service that is common. Moreover, the barrier of contacting someone is much higher if they need to install a new app and you cannot contact them because you don&rsquo;t have the right messenger.</p>
<p>While a social media platform is not a messenger, the effects are quite similar: <strong>The people using the big service essentially force you to use it too!</strong></p>
<h2 id="current-culture-in-the-fediverse">Current culture in the Fediverse</h2>
<p>The current culture in the Fediverse is a significant positive aspect. People in this community are exceptionally friendly compared to traditional social networks. Moreover, the Fediverse is highly inclusive. For instance, if you post an image without an alt text (allowing blind people to understand what the image is about), someone will certainly remind you to add it. This inclusivity is crucial to the community, emphasizing that everyone should have the opportunity to participate.</p>
<p>There&rsquo;s also a good amount of instances for LGBTQ+ to create safe spaces for those people. I consistently see <a href="https://www.fediverse.to/search/?category=lgbt">different instances</a> with this objective.</p>
<p>Of course, this depends on which edge of the Fediverse you are at. But overall, I think there are many minorities in the Fediverse who are not as represented elsewhere. I recall a poll someone conducted with around 11,000 votes, asking how many queer people there are in the Fediverse. The result was 60% not queer and 40% queer. These are solid numbers. Of course, this result is biased in some way because of who the poll maker is and who reposts it. But with this number of votes, it definitely gives a good hint.</p>

    <iframe title="Hypothesis: queers make up roughly or more than 50% of the active people on Fedi." src="https://fosstodon.org/@writeblankspace/111744426795021145/embed" class="mastodon-embed" style="max-width: 100%; border: 0;" width="100%" allowfullscreen="allowfullscreen"></iframe><script src="https://fosstodon.org/embed.js" async="async"></script>

    <br>
    <br>

<p>If someone wants to create artistic content that goes a bit beyond the guidelines of platforms like Instagram, it is acceptable in the Fediverse. You can choose an instance where the rules align with your preferences and create your content there. While most instances may require a content warning for specific posts, you are free to express yourself as you like.</p>
<p>Also, there is no need to upload regularly or avoid using specific words to please the algorithm. There is nothing like that here. There are no ads and therefore no advertisers which do not want you to say something &ldquo;bad&rdquo;. You can handle things how you and your instance want them to be.</p>
<p>Another immensely beneficial aspect here is that the moderator count per user is relatively high. This is because each instance is responsible for its moderation. As a result, every instance manages its part, leading to an overall high count of moderators. Moreover, if an instance becomes unmoderated, other servers will block them, ensuring a clean network.</p>
<p>But, after all, the Fediverse is currently a big home for people who are interested in technology. It is relatively new tech and because of that, there are a lot of nerds. Which is great!</p>
<p>Of course, these are not all groups of people, but I hope I could provide you with a bit of an overview of the kinds of people currently in the Fediverse. This may change as new people join us, but at the moment, this is how I perceive the Fediverse. Let&rsquo;s hope we can maintain this diversity for the future.</p>
<h3 id="who-is-in-the-fediverse">Who is in the Fediverse?</h3>
<p>For example, nearly all German institutions have an account on their own <a href="https://social.bund.de/about">server of the state</a>. Additionally, the public broadcasting providers <a href="https://ard.social/about">ARD</a> and <a href="https://zdf.social/about">ZDF</a> have their own instances.</p>
<p>I just saw that the <a href="https://social.overheid.nl/about">Netherlands</a> also have a state owned Mastodon instance. I really like the idea of nations backing the network and hopefully the values, this network comes with.</p>
<p>Mozilla also created its own instance available to the public: <a href="https://mozilla.social/about">Mozilla Social</a>. However, there are another 26,000 servers waiting for you to explore them.</p>
<p>Currently there are around 10,000,000 users in the Fediverse with a total of about 1,200,000 of them being active in the last month. So you hopefully you find someone to connect with.</p>
<p>A good overview of instances and the biggest users can be found at <a href="https://fedidb.org">FediDB</a>.</p>
<h2 id="platforms">Platforms</h2>
<p>In this section, I want to showcase a few of the most important services of the Fediverse. <strong>But there are many more to be explored!</strong></p>
<p>This image provides a quick overview of the different categories and services.</p>
<figure>
    <img loading="lazy" src="./fediverse-branches.webp"
         alt="This image shows a tree with the different branches of the Fediverse. It shows a few services like Mastodon, Pleroma, Pixelfed, Peertube, Castopod, Mobilizon and even more."/> <figcaption>
            <p><a href="https://axbom.com/">🄯 CC-BY-SA 3.0 by Per Axbom</a></p>
        </figcaption>
</figure>

<h3 id="mastodon">Mastodon</h3>
<p>Mastodon is the biggest and also the most developed platform of the Fediverse. It is a microblogging service. The company behind it is a German non-profit company (gGmbH). It is comparable with Twitter and gained a lot of attraction during <a href="https://www.theguardian.com/technology/2023/oct/27/elon-musk-x-twitter-takeover-revenue-users-advertising">the catastrophic takeover of Elon Musk</a>. At that time, there was a huge mass of people moving to Mastodon from Twitter.</p>
<p>After these events, the &ldquo;freed&rdquo; platform attracted <a href="https://www.theguardian.com/world/2023/jun/03/twitter-conservative-media-elon-musk-ron-desantis">even more extreme right-wing individuals</a>. Additionally, many <a href="https://www.theguardian.com/technology/2022/dec/16/twitter-elon-musk-suspension-journalists-sets-dangerous-precedent-un-warns">journalists face occasional blocks</a>. If you truly seek a free platform, consider joining Mastodon: <a href="https://joinmastodon.org">https://joinmastodon.org</a>.
(They also have a really fancy website: Check it out!)</p>
<figure>
    <img loading="lazy" src="./twitter-x.jpg"
         alt="This image shows a dead bird which should represent the Twitter bird. This bird is crossed with a iron beam. No they represent the new logo X."/> <figcaption>
            <p><a href="https://www.davidrevoy.com/">🄯 CC-BY 4.0 by David Revoy</a></p>
        </figcaption>
</figure>

<h3 id="pixelfed">Pixelfed</h3>
<p>Instagram is a really big platform run by Meta, which is known for its <a href="https://tuta.com/blog/posts/google-facebook-free">extremely bad privacy</a> and they also had a few scandals. For example, they helped manipulate elections. See <a href="https://www.theguardian.com/news/2018/mar/17/cambridge-analytica-facebook-influence-us-election">Cambridge Analytica</a>.</p>
<p>Pixelfed is the pardon from the Fediverse for Instagram. It is developed by a Canadian software engineer and the community.</p>
<p>In my opinion, it is not as widely developed as Mastodon. But it&rsquo;s almost on a perfect level. It has many features, even some that classical Instagram does not have. But most importantly, it is part of the Fediverse and therefore, you could also consume the great photography available on Pixelfed from your Mastodon feed or whichever service you like the most.</p>
<p>And of course, Pixelfed is developed as <strong>Free and Open Source Software / FOSS!</strong></p>
<p>You can join it at: <a href="https://pixelfed.org/">https://pixelfed.org/</a></p>
<h3 id="lemmy">Lemmy</h3>
<p>Lemmy is Reddit for the Fediverse and it is also <strong>FOSS</strong>. It seems to have a really active community. I personally did not try it out that much yet but it seems pretty good.</p>
<p>After all, the decision to use Lemmy is not that difficult if Reddit is the current choice. Reddit did screw up hard this year. Same as Twitter. They <a href="https://www.pbs.org/newshour/economy/despite-widespread-user-protest-reddit-ceo-says-company-is-not-negotiating-on-3rd-party-app-charges">banned every third-party app</a> and also made a couple of other bad decisions. On the image below, you see a Reddit event where the collective of Reddit users has written &ldquo;Fuck spez!&rdquo; which is the CEO of Reddit.</p>
<figure>
    <img loading="lazy" src="./fuck-spez.png"
         alt="There is written Fuck spez! on the r/place canvas."/> 
</figure>

<p>This is a great example that the community cannot do anything against the will of a platform like Reddit, Twitter, Instagram, TikTok&hellip; They are extremely powerful companies, maybe monopolies, exploiting their power of having all the users. Let&rsquo;s break that! Let&rsquo;s make a real change and go to the Fediverse!</p>
<p>If you are interested in Lemmy: <a href="https://join-lemmy.org/">https://join-lemmy.org/</a></p>
<h3 id="peertube">Peertube</h3>
<p>Peertube is an alternative to YouTube. Because video streaming is quite expensive and in a decentralized network, there are many smaller communities with less money. Peertube decided to use <em>peer</em> to <em>peer</em>. This means if you watch a video, you help someone else watching because your computer sends bits of the video to other persons. So the server does need less bandwidth and therefore, small communities are able to operate a video streaming platform. This approach comes with downsides, but after all, it makes something possible, which would be really hard to achieve in another way. <strong>A really free video platform.</strong></p>
<p>In terms of content, this platform is still a bit niche. There are really good channels, especially in the privacy, tech and Linux community. But for more mainstream content, there is not that much to offer. This, of course, is not a problem of the platform but a problem of the creators who are not feeding the platform. I hope this will change in the future!</p>
<p>Because of the Fediverse, you can, of course, watch and subscribe to Peertube channels from Mastodon and other platforms.</p>
<p>Peertube is developed by the French non-profit association <a href="https://framasoft.org/en/">Framasoft</a> as <strong>FOSS</strong>.</p>
<p>Here you can find more information: <a href="https://joinpeertube.org/">https://joinpeertube.org/</a></p>
<h3 id="what-about-threads-meta">What about Threads (Meta)?</h3>
<figure>
    <img loading="lazy" src="./meta-red-carpet.jpg"
         alt="A little Mastodon is rolling out a red carpet for Meta represented as death / a reaper."/> <figcaption>
            <p><a href="https://www.davidrevoy.com/">🄯 CC-BY 4.0 by David Revoy</a></p>
        </figcaption>
</figure>

<p>Now, bigger companies are starting to join the Fediverse. The biggest is Threads made by Meta.</p>
<p>Currently, Threads is implementing the required  <a href="../../../tags/activitypub">ActivityPub</a> logic to join the network. You can already follow the CEO of Instagram and a few of the team. But it seems like the full connection will take a while to implement.</p>
<p>This topic is a controversy in the Fediverse. On one side stands that Meta is bringing a lot of users to the network, but on the other side, Meta is also well-known for incredibly bad privacy and being part of surveillance capitalism, hurting millions of users.</p>
<p>A few instances have even made the <a href="https://fedidb.org/current-events/anti-meta-fedi-pact">&ldquo;Fedi-Pact&rdquo;</a> and therefore will block Meta on the server side when they are joining. This has an impact of about 7% of active users on the open side of the Fediverse. (Of course, only Meta will not see them. The rest of the network can still interact with them normally.)</p>
<p>If a server does not block a specific other server, the user still has the power to do that. Here, the founder of Mastodon explains his audience how it is done.</p>

    <iframe title="Block Threads" src="https://mastodon.social/@Gargron/111587088958531028/embed" class="mastodon-embed" style="max-width: 100%; border: 0" width="100%" allowfullscreen="allowfullscreen"></iframe><script src="https://mastodon.social/embed.js" async="async"></script>

<p>And, of course, Meta will not provide a platform that is nearly as free and open as the other ones I mentioned above. They are only trying to make money and hopefully are not destroying the Fediverse while doing that. The only good they do is that they bring a lot of users with them. That&rsquo;s it. But this also may open the Fediverse to a lot of people, which would be great.</p>
<p>Meta joining is like a dance with the devil. But we may benefit from it.</p>
<h2 id="also-worth-mentioning">Also worth mentioning</h2>
<p>You are not stuck with your account on a server. Of course, you have the freedom to choose another one and take all your followers with you. No problem here. Free software doesn&rsquo;t lock you in place.</p>
<p>Another thing getting weird in the old networks is the account verification. You now need to pay in both <a href="https://www.pcmag.com/news/paid-verification-for-facebook-instagram-starts-rolling-out-in-us">Instagram</a> and <a href="https://www.businessinsider.com/twitter-verification-abuse-trolls-parody-george-bush-oj-simpson-confession-2022-11?IR=T">Twitter</a> to be verified, even if you are not a big creator or another person of interest. This is rather stupid and confusing. In the Fediverse, you do not need to pay anyone. You can just link yourself from another platform/website you own and you get a sign that this account/page is verified to be owned by you. A much better way of verification.</p>
<h2 id="the-technical-stuff">The technical stuff</h2>
<p>I decided not to put the really technical stuff in this post. This would have made this blog post even longer than it is now. I have a bit of experience in creating servers for the Fediverse because of <a href="../../projects/whathappenedtofedodo">Fedodo</a>. I want to share this experience in a future post and talk about the details of  <a href="../../../tags/activitypub">ActivityPub</a>. This is the protocol powering most of the Fediverse.</p>
<h2 id="appreciations">Appreciations</h2>
<p>Thanks to the amazing artists providing their work under Creative Commons licenses. They help me so much in illustrating this post. Links are available under each image. Go and check them out!</p>
<p>Also, thanks to you for reading this until the end. I hope I could give you a bit of an overview of the topic. And maybe I even convinced you to join us fighting for a better web. But don&rsquo;t pressure yourself. Have fun ❤️</p>
]]></content:encoded>
    </item>
  </channel>
</rss>