<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:base="en">
	<title>andrés ignacio torres</title>
	<subtitle>A software engineer rambling about code, literature and everything in between.</subtitle>
	<link href="https://aitorres.com/feed/feed.xml" rel="self"/>
	<link href="https://aitorres.com/"/>
	<updated>2025-12-16T00:00:00Z</updated>
	<id>https://aitorres.com</id>
	<author>
		<name>Andrés Ignacio Torres</name>
		<email>andres@aitorres.com</email>
	</author>
	
	<entry>
		<title>Connect Ozone to your PDS</title>
		<link href="https://aitorres.com/blog/connect-ozone-to-your-pds/"/>
		<updated>2025-12-16T00:00:00Z</updated>
		<id>https://aitorres.com/blog/connect-ozone-to-your-pds/</id>
		<content type="html">&lt;p&gt;If you are self-hosting both a PDS and
&lt;a href=&quot;https://aitorres.com/blog/self-host-bluesky-ozone-alongside-other-services&quot;&gt;an Ozone instance&lt;/a&gt;, you
will want to set up these environment variables on your PDS so that your PDS and
Ozone can correctly talk to each other, in particular so Ozone can send email
notifications via your PDS SMTP settings, as well as act as its &amp;quot;moderation
service&amp;quot;.&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot; tabIndex=&quot;0&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# on your pds.env or your PDS container environment&lt;/span&gt;
&lt;span class=&quot;token assign-left variable&quot;&gt;PDS_MOD_SERVICE_DID&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;&amp;lt;your Ozone&#39;s service account DID&gt;&quot;&lt;/span&gt;
&lt;span class=&quot;token assign-left variable&quot;&gt;PDS_MOD_SERVICE_URL&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;&amp;lt;URL to your Ozone deployment&gt;&quot;&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;# note that even if you have PDS_EMAIL_SMTP_URL and&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;# PDS_EMAIL_FROM_ADDRESS set, these will not be used&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;# by Ozone unless you also set these two&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;# (you can reuse the same values):&lt;/span&gt;
&lt;span class=&quot;token assign-left variable&quot;&gt;PDS_MODERATION_EMAIL_SMTP_URL&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;&amp;lt;SMTP server to send emails from&gt;&quot;&lt;/span&gt;
&lt;span class=&quot;token assign-left variable&quot;&gt;PDS_MODERATION_EMAIL_ADDRESS&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;&amp;lt;email address to send moderation emails from&gt;&quot;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Thanks to &lt;a href=&quot;https://bsky.app/profile/baileytownsend.dev&quot;&gt;@baileytownsend.dev&lt;/a&gt; for
helping troubleshoot and figure this out, and to
&lt;a href=&quot;https://bsky.app/profile/thisismissem.social&quot;&gt;@thisismissem.social&lt;/a&gt; for calling
it out as well on this helpful thread!&lt;/p&gt;
&lt;blockquote class=&quot;bluesky-embed&quot; data-bluesky-uri=&quot;at://did:plc:5w4eqcxzw5jv5qfnmzxcakfy/app.bsky.feed.post/3m5j5f542422y&quot; data-bluesky-cid=&quot;bafyreicyth6xz6rgj7p7qabgtbx44zaw72ubg73ns6bk4ujstxtuw3zii4&quot; data-bluesky-embed-color-mode=&quot;system&quot;&gt;&lt;p lang=&quot;en&quot;&gt;Just realised many self-hosted PDSes likely don&amp;#x27;t have moderation emails enabled because email config doesn&amp;#x27;t cascade to moderation email config, so you have to specify both 🤔&lt;/p&gt;&amp;mdash; Emelia (&lt;a href=&quot;https://bsky.app/profile/did:plc:5w4eqcxzw5jv5qfnmzxcakfy?ref_src=embed&quot;&gt;@thisismissem.social&lt;/a&gt;) &lt;a href=&quot;https://bsky.app/profile/did:plc:5w4eqcxzw5jv5qfnmzxcakfy/post/3m5j5f542422y?ref_src=embed&quot;&gt;November 13, 2025 at 4:15 AM&lt;/a&gt;&lt;/blockquote&gt;&lt;script async=&quot;&quot; src=&quot;https://embed.bsky.app/static/embed.js&quot; charset=&quot;utf-8&quot;&gt;&lt;/script&gt;
</content>
	</entry>
	
	<entry>
		<title>On Kubernetes volumes</title>
		<link href="https://aitorres.com/blog/on-kubernetes-volumes/"/>
		<updated>2025-11-27T00:00:00Z</updated>
		<id>https://aitorres.com/blog/on-kubernetes-volumes/</id>
		<content type="html">&lt;p&gt;This week I had to recreate a Kubernetes cluster to mitigate an incident on a
side project.&lt;/p&gt;
&lt;blockquote class=&quot;bluesky-embed&quot; data-bluesky-uri=&quot;at://did:plc:3nlkmby2zllrhcj6z5dnicui/app.bsky.feed.post/3m6fa2bc6322f&quot; data-bluesky-cid=&quot;bafyreid64g36ur2j6btxgr7cdxosqaklm7bwts6ctc6dhbcryubar2to7a&quot; data-bluesky-embed-color-mode=&quot;system&quot;&gt;&lt;p lang=&quot;en&quot;&gt;waking up to find the whole nodepool unexplicably down! oh no!&lt;/p&gt;&amp;mdash; andrés ignacio torres (&lt;a href=&quot;https://bsky.app/profile/did:plc:3nlkmby2zllrhcj6z5dnicui?ref_src=embed&quot;&gt;@andresitorresm.com&lt;/a&gt;) &lt;a href=&quot;https://bsky.app/profile/did:plc:3nlkmby2zllrhcj6z5dnicui/post/3m6fa2bc6322f?ref_src=embed&quot;&gt;November 24, 2025 at 8:17 AM&lt;/a&gt;&lt;/blockquote&gt;&lt;script async=&quot;&quot; src=&quot;https://embed.bsky.app/static/embed.js&quot; charset=&quot;utf-8&quot;&gt;&lt;/script&gt;
&lt;p&gt;As I&#39;m still learning my way through Kubernetes concepts, I thought it&#39;d be
useful (to myself!) to write a quick note on what I&#39;ve learned about volumes in
Kubernetes and how they relate to each other.&lt;/p&gt;
&lt;h2 id=&quot;persistent-volumes-claims-pvcs&quot; tabindex=&quot;-1&quot;&gt;Persistent Volumes Claims (PVCs) &lt;a class=&quot;header-anchor&quot; href=&quot;https://aitorres.com/blog/on-kubernetes-volumes/#persistent-volumes-claims-pvcs&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Essentially, this is a request for storage. You request a given amount of
storage, and a storage class for it, and Kubernetes will manage that for you so
you don&#39;t have to provision the disk yourself (at least in a managed Kubernetes
environment).&lt;/p&gt;
&lt;p&gt;As long as the persistent volume claim exists, the storage volume will be kept
around (persistent), even if no pod is using it, or if the pod using it is
deleted.&lt;/p&gt;
&lt;p&gt;Check your cluster&#39;s persistent volume claims with:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot; tabIndex=&quot;0&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;kubectl get pvc&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Read more
&lt;a href=&quot;https://kubernetes.io/docs/concepts/storage/persistent-volumes/#persistentvolumeclaims&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id=&quot;persistent-volumes-pvs&quot; tabindex=&quot;-1&quot;&gt;Persistent Volumes (PVs) &lt;a class=&quot;header-anchor&quot; href=&quot;https://aitorres.com/blog/on-kubernetes-volumes/#persistent-volumes-pvs&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;This is the actual storage volume that gets created automatically to comply with
a persistent volume claim, or it can also be created manually.&lt;/p&gt;
&lt;p&gt;If it was created automatically, it will also be deleted automatically when the
claim is deleted. Important!&lt;/p&gt;
&lt;p&gt;Check your cluster&#39;s persistent volumes with:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot; tabIndex=&quot;0&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;kubectl get &lt;span class=&quot;token function&quot;&gt;pv&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Read more
&lt;a href=&quot;https://kubernetes.io/docs/concepts/storage/persistent-volumes/#persistent-volumes&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id=&quot;volume-attachments&quot; tabindex=&quot;-1&quot;&gt;Volume Attachments &lt;a class=&quot;header-anchor&quot; href=&quot;https://aitorres.com/blog/on-kubernetes-volumes/#volume-attachments&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;This is a resource that Kubernetes creates and uses to track which volume is
attached to which node. When a pod that uses a given persistent volume claim is
scheduled to a node, Kubernetes will create a volume attachment to signify that
the volume is attached to that node.&lt;/p&gt;
&lt;p&gt;Check your cluster&#39;s volume attachments with:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot; tabIndex=&quot;0&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;kubectl get volumeattachments&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Read more
&lt;a href=&quot;https://kubernetes.io/docs/reference/kubernetes-api/config-and-storage-resources/volume-attachment-v1/&quot;&gt;here&lt;/a&gt;&lt;/p&gt;
&lt;h2 id=&quot;can-i-delete&quot; tabindex=&quot;-1&quot;&gt;Can I delete...? &lt;a class=&quot;header-anchor&quot; href=&quot;https://aitorres.com/blog/on-kubernetes-volumes/#can-i-delete&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;A persistent volume claim? Yes, but if the volume tied to it was created
automatically, the volume will be deleted too.&lt;/p&gt;
&lt;p&gt;A persistent volume? Only if no persistent volume claim is using it, and this
will delete all data stored on it.&lt;/p&gt;
&lt;p&gt;A volume attachment? Yes, but you most likely won&#39;t need to. Unless you&#39;re me, a
few days ago, and had to force a volume to detach from a node because it was
stuck. In any case, deleting a volume attachment will NOT delete the volume
itself, or the data on it.&lt;/p&gt;
&lt;p class=&quot;post-footer&quot;&gt;
  I am not a Kubernetes expert, so please double check the official documentation
  before operating on your cluster!
&lt;/p&gt;
</content>
	</entry>
	
	<entry>
		<title>I went to FediCon 2025!</title>
		<link href="https://aitorres.com/blog/i-went-to-fedicon-2025/"/>
		<updated>2025-09-01T00:00:00Z</updated>
		<id>https://aitorres.com/blog/i-went-to-fedicon-2025/</id>
		<content type="html">&lt;p&gt;&lt;picture&gt;&lt;source type=&quot;image/webp&quot; srcset=&quot;https://aitorres.com/img/fedicon-2025-1.webp 320w, https://aitorres.com/img/fedicon-2025-1.webp 640w, https://aitorres.com/img/fedicon-2025-1.webp 960w, https://aitorres.com/img/fedicon-2025-1.webp 1280w, https://aitorres.com/img/fedicon-2025-1.webp 1600w, https://aitorres.com/img/fedicon-2025-1.webp 3013w&quot; sizes=&quot;100vw&quot;&gt;&lt;source type=&quot;image/png&quot; srcset=&quot;https://aitorres.com/img/fedicon-2025-1.png 320w, https://aitorres.com/img/fedicon-2025-1.png 640w, https://aitorres.com/img/fedicon-2025-1.png 960w, https://aitorres.com/img/fedicon-2025-1.png 1280w, https://aitorres.com/img/fedicon-2025-1.png 1600w, https://aitorres.com/img/fedicon-2025-1.png 3013w&quot; sizes=&quot;100vw&quot;&gt;&lt;img src=&quot;https://aitorres.com/img/fedicon-2025-1.jpeg&quot; alt=&quot;Photo of the stage at FediCon 2025. The organizer is giving a speech while an image saying &#39;Welcome to FediCon 2025&#39; is projected&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; width=&quot;3013&quot; height=&quot;2093&quot; srcset=&quot;https://aitorres.com/img/fedicon-2025-1.jpeg 320w, https://aitorres.com/img/fedicon-2025-1.jpeg 640w, https://aitorres.com/img/fedicon-2025-1.jpeg 960w, https://aitorres.com/img/fedicon-2025-1.jpeg 1280w, https://aitorres.com/img/fedicon-2025-1.jpeg 1600w, https://aitorres.com/img/fedicon-2025-1.jpeg 3013w&quot; sizes=&quot;100vw&quot;&gt;&lt;/picture&gt;&lt;/p&gt;
&lt;figcaption&gt;Welcome speech at FediCon 2025.&lt;/figcaption&gt;
&lt;div style=&quot;text-align: right;&quot;&gt;
    &lt;i&gt;What if the Fediverse met... in real life?&lt;/i&gt;
&lt;/div&gt;
&lt;p&gt;Last month, I had the pleasure of attending &lt;a href=&quot;https://fedicon.ca/&quot;&gt;FediCon 2025&lt;/a&gt;,
a local conference for &amp;quot;decentralized social networks and the social web&amp;quot;, held
here in Vancouver, Canada.&lt;/p&gt;
&lt;p&gt;I learned about FediCon a while ago through my Mastodon feed, and I signed up
for it right away (I didn&#39;t have to think much about it, given that the
conference chose a venue just a few minutes from home!).&lt;/p&gt;
&lt;p&gt;Even though I&#39;ve gravitated mostly towards Bluesky in recent months, my journey
through decentralized and federated social networks started with Mastodon and
ActivityPub, so it&#39;s got a soft spot in my heart.&lt;/p&gt;
&lt;p&gt;&lt;picture&gt;&lt;source type=&quot;image/webp&quot; srcset=&quot;https://aitorres.com/img/fedicon-2025-2.webp 320w, https://aitorres.com/img/fedicon-2025-2.webp 640w, https://aitorres.com/img/fedicon-2025-2.webp 960w, https://aitorres.com/img/fedicon-2025-2.webp 1280w, https://aitorres.com/img/fedicon-2025-2.webp 1600w, https://aitorres.com/img/fedicon-2025-2.webp 3023w&quot; sizes=&quot;100vw&quot;&gt;&lt;source type=&quot;image/png&quot; srcset=&quot;https://aitorres.com/img/fedicon-2025-2.png 320w, https://aitorres.com/img/fedicon-2025-2.png 640w, https://aitorres.com/img/fedicon-2025-2.png 960w, https://aitorres.com/img/fedicon-2025-2.png 1280w, https://aitorres.com/img/fedicon-2025-2.png 1600w, https://aitorres.com/img/fedicon-2025-2.png 3023w&quot; sizes=&quot;100vw&quot;&gt;&lt;img src=&quot;https://aitorres.com/img/fedicon-2025-2.jpeg&quot; alt=&quot;Photo of the stage at FediCon 2025. Johannes Ernst is giving a talk.&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; width=&quot;3023&quot; height=&quot;2434&quot; srcset=&quot;https://aitorres.com/img/fedicon-2025-2.jpeg 320w, https://aitorres.com/img/fedicon-2025-2.jpeg 640w, https://aitorres.com/img/fedicon-2025-2.jpeg 960w, https://aitorres.com/img/fedicon-2025-2.jpeg 1280w, https://aitorres.com/img/fedicon-2025-2.jpeg 1600w, https://aitorres.com/img/fedicon-2025-2.jpeg 3023w&quot; sizes=&quot;100vw&quot;&gt;&lt;/picture&gt;&lt;/p&gt;
&lt;figcaption&gt;Johannes Ernst on his talk &quot;From Millions To Billions: a Plausible Narrative for How to Grow the Open Social Web&quot;.&lt;/figcaption&gt;
&lt;p&gt;And what a great experience it was! I got to learn from some amazing speakers
and prominent personalities in the Fediverse space, including one of
ActivityPub&#39;s co-creators (Evan Prodmorou).&lt;/p&gt;
&lt;p&gt;Contrary to what I originally expected, the conference didn&#39;t &lt;em&gt;just&lt;/em&gt; focus on
ActivityPub (although the protocol took a central role in most talks); one of
the speakers was &lt;a href=&quot;https://anew.social&quot;&gt;Anuj Ahoooja&lt;/a&gt;, who spoke about bridging
decentralized platforms based on his experience working on
&lt;a href=&quot;https://brid.gy/&quot;&gt;Brid.gy&lt;/a&gt;, as well as an amazing survey of unique, innovative
and creative ATProto applications that go beyond just microblogging by our local
star, &lt;a href=&quot;https://bmannconsulting.com/&quot;&gt;Boris Mann&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;picture&gt;&lt;source type=&quot;image/webp&quot; srcset=&quot;https://aitorres.com/img/fedicon-2025-3.webp 320w, https://aitorres.com/img/fedicon-2025-3.webp 640w, https://aitorres.com/img/fedicon-2025-3.webp 960w, https://aitorres.com/img/fedicon-2025-3.webp 1280w, https://aitorres.com/img/fedicon-2025-3.webp 1600w, https://aitorres.com/img/fedicon-2025-3.webp 3024w&quot; sizes=&quot;100vw&quot;&gt;&lt;source type=&quot;image/png&quot; srcset=&quot;https://aitorres.com/img/fedicon-2025-3.png 320w, https://aitorres.com/img/fedicon-2025-3.png 640w, https://aitorres.com/img/fedicon-2025-3.png 960w, https://aitorres.com/img/fedicon-2025-3.png 1280w, https://aitorres.com/img/fedicon-2025-3.png 1600w, https://aitorres.com/img/fedicon-2025-3.png 3024w&quot; sizes=&quot;100vw&quot;&gt;&lt;img src=&quot;https://aitorres.com/img/fedicon-2025-3.jpeg&quot; alt=&quot;Photo of the stage at FediCon 2025. Boris Mann is giving a talk.&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; width=&quot;3024&quot; height=&quot;2091&quot; srcset=&quot;https://aitorres.com/img/fedicon-2025-3.jpeg 320w, https://aitorres.com/img/fedicon-2025-3.jpeg 640w, https://aitorres.com/img/fedicon-2025-3.jpeg 960w, https://aitorres.com/img/fedicon-2025-3.jpeg 1280w, https://aitorres.com/img/fedicon-2025-3.jpeg 1600w, https://aitorres.com/img/fedicon-2025-3.jpeg 3024w&quot; sizes=&quot;100vw&quot;&gt;&lt;/picture&gt;&lt;/p&gt;
&lt;figcaption&gt;Boris Mann on his talk &quot;Beyond Microblogging: AT Protocol for Building Unique Social Apps&quot;.&lt;/figcaption&gt;
&lt;p&gt;Another highlight for me was hearing first-hand experiences on how ActivityPub
and the Fediverse are being used in local contexts for community building, data
sovereignty and digital self-determination, with talks on how CoSocial and
SocialBC (two Mastodon instances tailor-made for Canadians on a provincial and
federal level) came to be and are managed, and even a non-technical talk about
the Fediverse as a digital third space that can enable &amp;quot;community
(re)connection&amp;quot;.&lt;/p&gt;
&lt;p&gt;These talks, as well as the rest, were all refreshing and inspiring, with a mix
of historical background around the Fediverse, and projections towards the
future by looking at a lot of creative uses of the Fediverse in order to empower
communities and individuals.&lt;/p&gt;
&lt;p&gt;I left the conference with a lot of ideas and motivation to keep exploring how
to build and contribute to a decentralized and social web, no matter the
underlying platform or protocol (or, more specifically, &lt;em&gt;not limited&lt;/em&gt; by them).&lt;/p&gt;
&lt;p&gt;You can watch all the recorded sessions on
&lt;a href=&quot;https://spectra.video/w/p/52g7HzMoUCxEm9nU339mPf?playlistPosition=1&quot;&gt;FediCon&#39;s official Spectra Video (PeerTube) channel&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Looking forward to FediCon 2026!&lt;/p&gt;
</content>
	</entry>
	
	<entry>
		<title>Small spaces</title>
		<link href="https://aitorres.com/blog/small-spaces/"/>
		<updated>2025-07-29T00:00:00Z</updated>
		<id>https://aitorres.com/blog/small-spaces/</id>
		<content type="html">&lt;p&gt;&lt;picture&gt;&lt;source type=&quot;image/webp&quot; srcset=&quot;https://aitorres.com/img/port-moody-park-stairs.webp 320w, https://aitorres.com/img/port-moody-park-stairs.webp 640w, https://aitorres.com/img/port-moody-park-stairs.webp 960w, https://aitorres.com/img/port-moody-park-stairs.webp 993w&quot; sizes=&quot;100vw&quot;&gt;&lt;img src=&quot;https://aitorres.com/img/port-moody-park-stairs.png&quot; alt=&quot;Photograph of a wooden path with steps in a forest, with multiple yellow and brown leaves on the ground, suggesting it has rained.&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; width=&quot;993&quot; height=&quot;773&quot; srcset=&quot;https://aitorres.com/img/port-moody-park-stairs.png 320w, https://aitorres.com/img/port-moody-park-stairs.png 640w, https://aitorres.com/img/port-moody-park-stairs.png 960w, https://aitorres.com/img/port-moody-park-stairs.png 993w&quot; sizes=&quot;100vw&quot;&gt;&lt;/picture&gt;&lt;/p&gt;
&lt;figcaption&gt;From a short trip to Port Moody, B.C. (2023)&lt;/figcaption&gt;
&lt;p&gt;A couple months ago, I went to a poetry workshop offered for migrants by
Vancouver Writers Fest, led by Evelyn Lau. We read, we laughed, we talked, we
cried, but most importantly, within those four hours we created a bond, built a
community and met others who, for one reason or another, shared at least two
threads in common with me: being a writer and being a migrant.&lt;/p&gt;
&lt;p&gt;Since last year, I&#39;ve attended multiple creative writing workshops, but this one
hit different. For the first time, I&#39;ve found myself meeting some of the
participants again a few days later, at another event. Then again, and again.
And little by little, they have helped me cultivate a small space of belonging
in a city that doesn&#39;t belong to me, and to which I don&#39;t belong either, not
entirely.&lt;/p&gt;
&lt;p&gt;Between verses and books and laughter and tears (and fingers snapping), I have
found a breathing room, a subterfuge from the daily grind, to which I cling and
in which I find a push, sometmes very much needed, to put my eyes on the sund
beyond the grayest of the clouds.&lt;/p&gt;
&lt;p&gt;So it seems that the pieces, little by little, end up falling into place.&lt;/p&gt;
</content>
	</entry>
	
	<entry>
		<title>Getting OVH Public Cloud multi-region connectivity to work</title>
		<link href="https://aitorres.com/blog/ovh-cloud-multi-region-connectivity/"/>
		<updated>2025-07-14T00:00:00Z</updated>
		<id>https://aitorres.com/blog/ovh-cloud-multi-region-connectivity/</id>
		<content type="html">&lt;p&gt;I&#39;ve been working on a very cool passion project recently that involves setting
up cloud infrastructure on
&lt;a href=&quot;https://www.ovhcloud.com/en-ca/public-cloud/&quot;&gt;OVH Public Cloud&lt;/a&gt; for data
sovereignty. As part of the infrastructure design, I needed to set up one
resource in a given region and another resource in a different region, and allow
them to communicate with each other over private IPs.&lt;/p&gt;
&lt;p&gt;This is a common use case for cloud networking and I&#39;ve had to do this many
times before on other cloud providers (Azure, AWS, GCP...), but my intuition and
previous experience did not help me much when I tried to replicate this on OVH
Public Cloud.&lt;/p&gt;
&lt;blockquote class=&quot;bluesky-embed&quot; data-bluesky-uri=&quot;at://did:plc:3nlkmby2zllrhcj6z5dnicui/app.bsky.feed.post/3lt4ojhiy7k2k&quot; data-bluesky-cid=&quot;bafyreib7qbuukxh6uimsaymzgbbu4i5hgnk7yvu2s4p56h25ftre4tjkf4&quot; data-bluesky-embed-color-mode=&quot;system&quot;&gt;&lt;p lang=&quot;en&quot;&gt;i suspect dns is at fault. it’s always dns.&lt;/p&gt;&amp;mdash; andrés ignacio torres (&lt;a href=&quot;https://bsky.app/profile/did:plc:3nlkmby2zllrhcj6z5dnicui?ref_src=embed&quot;&gt;@andresitorresm.com&lt;/a&gt;) &lt;a href=&quot;https://bsky.app/profile/did:plc:3nlkmby2zllrhcj6z5dnicui/post/3lt4ojhiy7k2k?ref_src=embed&quot;&gt;July 4, 2025 at 12:05 AM&lt;/a&gt;&lt;/blockquote&gt;&lt;script async=&quot;&quot; src=&quot;https://embed.bsky.app/static/embed.js&quot; charset=&quot;utf-8&quot;&gt;&lt;/script&gt;
&lt;p&gt;Naively, after skimming OVH&#39;s documentation, my first attempt looked something
like this (on Terraform):&lt;/p&gt;
&lt;pre class=&quot;language-hcl&quot; tabIndex=&quot;0&quot;&gt;&lt;code class=&quot;language-hcl&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# One private network&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;resource &lt;span class=&quot;token type variable&quot;&gt;&quot;ovh_cloud_project_network_private&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;network&quot;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;service_name&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;abc123&quot;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;name&lt;/span&gt;         &lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;multiregion-network&quot;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;regions&lt;/span&gt;      &lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;GRA1&quot;&lt;/span&gt;, &lt;span class=&quot;token string&quot;&gt;&quot;BHS1&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;# Two subnets in the same private network, one&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;# in each region&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;resource &lt;span class=&quot;token type variable&quot;&gt;&quot;ovh_cloud_project_network_private_subnet&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;subnet1&quot;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;service_name&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;abc123&quot;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;network_id&lt;/span&gt;   &lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt; ovh_cloud_project_network_private.network.id
  &lt;span class=&quot;token property&quot;&gt;region&lt;/span&gt;       &lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;GRA1&quot;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;start&lt;/span&gt;        &lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;10.0.1.1&quot;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;end&lt;/span&gt;          &lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;10.0.1.255&quot;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;network&lt;/span&gt;      &lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;10.0.1.0/24&quot;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;dhcp&lt;/span&gt;         &lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;no_gateway&lt;/span&gt;   &lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;false&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;resource &lt;span class=&quot;token type variable&quot;&gt;&quot;ovh_cloud_project_network_private_subnet&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;subnet2&quot;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;service_name&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;abc123&quot;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;network_id&lt;/span&gt;   &lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt; ovh_cloud_project_network_private.network.id
  &lt;span class=&quot;token property&quot;&gt;region&lt;/span&gt;       &lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;BHS1&quot;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;start&lt;/span&gt;        &lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;10.0.2.1&quot;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;end&lt;/span&gt;          &lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;10.0.2.255&quot;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;network&lt;/span&gt;      &lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;10.0.2.0/24&quot;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;dhcp&lt;/span&gt;         &lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;no_gateway&lt;/span&gt;   &lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;false&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This seemed to me like it would work, and I didn&#39;t get any errors when applying
the Terraform configuration. However, the resources deployed in each region&#39;s
subnet could not communicate with each other over their private IPs. This was
confusing and I didn&#39;t find any multi-region examples in OVH&#39;s documentation or
Github.&lt;/p&gt;
&lt;p&gt;Among other things, I tried separating the private network into two different
resources, one for each region (similar to how AWS would require two VPCs) but
that didn&#39;t work either (and it didn&#39;t make sense, since OVH&#39;s documentation
allowed you to create a single private network across multiple regions).&lt;/p&gt;
&lt;p&gt;After a lot of digging and reading through OVH&#39;s documentation, I found
&lt;a href=&quot;https://help.ovhcloud.com/csm/en-ca-public-cloud-network-concepts?id=kb_article_view&amp;amp;sysparm_article=KB0050143#:~:text=Multi%2Dregion%20private%20networks%3A%20To%20achieve%20resource%20interconnectivity%20between%20regions%2C%20the%20same%20vRack/VLAN/subnet%20should%20be%20used%20to%20create%20a%20private%20network%20in%20each%20region.%20Please%20note%20that%20different%20DHCP%20IP%20ranges%20must%20be%20used%20in%20different%20regions.&quot;&gt;one key line&lt;/a&gt;
that explained the issue (emphasis mine):&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Multi-region private networks: To achieve resource interconnectivity between
regions, &lt;strong&gt;the same vRack/VLAN/subnet&lt;/strong&gt; should be used to create a private
network in each region. Please note that &lt;strong&gt;different DHCP IP ranges must be
used in different regions&lt;/strong&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Aha! The implication here is that I needed to use &amp;quot;the same subnet&amp;quot; in each
region, and this translates to using the same &lt;code&gt;network&lt;/code&gt; CIDR in each region&#39;s
subnet resource, while keeping the &lt;code&gt;start&lt;/code&gt; and &lt;code&gt;end&lt;/code&gt; IPs different in each
region to avoid conflicts.&lt;/p&gt;
&lt;p&gt;My updated Terraform configuration looks like this:&lt;/p&gt;
&lt;pre class=&quot;language-hcl&quot; tabIndex=&quot;0&quot;&gt;&lt;code class=&quot;language-hcl&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# One private network&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;resource &lt;span class=&quot;token type variable&quot;&gt;&quot;ovh_cloud_project_network_private&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;network&quot;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;service_name&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;abc123&quot;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;name&lt;/span&gt;         &lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;multiregion-network&quot;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;regions&lt;/span&gt;      &lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;GRA1&quot;&lt;/span&gt;, &lt;span class=&quot;token string&quot;&gt;&quot;BHS1&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;# Two subnets in the same private network that share&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;# the same network CIDR, but have different DHCP ranges&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;resource &lt;span class=&quot;token type variable&quot;&gt;&quot;ovh_cloud_project_network_private_subnet&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;subnet1&quot;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;service_name&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;abc123&quot;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;network_id&lt;/span&gt;   &lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt; ovh_cloud_project_network_private.network.id
  &lt;span class=&quot;token property&quot;&gt;region&lt;/span&gt;       &lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;GRA1&quot;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;start&lt;/span&gt;        &lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;10.0.1.1&quot;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;end&lt;/span&gt;          &lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;10.0.1.155&quot;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;network&lt;/span&gt;      &lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;10.0.1.0/24&quot;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;dhcp&lt;/span&gt;         &lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;no_gateway&lt;/span&gt;   &lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;false&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;resource &lt;span class=&quot;token type variable&quot;&gt;&quot;ovh_cloud_project_network_private_subnet&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;subnet2&quot;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;service_name&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;abc123&quot;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;network_id&lt;/span&gt;   &lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt; ovh_cloud_project_network_private.network.id
  &lt;span class=&quot;token property&quot;&gt;region&lt;/span&gt;       &lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;BHS1&quot;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;start&lt;/span&gt;        &lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;10.0.1.156&quot;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;end&lt;/span&gt;          &lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;10.0.1.255&quot;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;network&lt;/span&gt;      &lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;10.0.1.0/24&quot;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;dhcp&lt;/span&gt;         &lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;no_gateway&lt;/span&gt;   &lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;false&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This change was enough to get the resources in each region to communicate with
each other over their private IPs!&lt;/p&gt;
</content>
	</entry>
	
	<entry>
		<title>Self-hosting Bluesky&#39;s Ozone alongside other services</title>
		<link href="https://aitorres.com/blog/self-host-bluesky-ozone-alongside-other-services/"/>
		<updated>2025-05-19T00:00:00Z</updated>
		<id>https://aitorres.com/blog/self-host-bluesky-ozone-alongside-other-services/</id>
		<content type="html">&lt;p class=&quot;post-header-note&quot;&gt;
  Shoutout to &lt;a href=&quot;https://cprimozic.net/&quot; title=&quot;Casey Primozic&quot;&gt;Casey Primozic&lt;/a&gt; (&lt;a href=&quot;https://bsky.app/profile/ameo.dev&quot; title=&quot;@ameo.dev bluesky profile&quot;&gt;@ameo.dev&lt;/a&gt;) for the wonderful and detailed &lt;a href=&quot;https://cprimozic.net/notes/posts/notes-on-self-hosting-bluesky-pds-alongside-other-services/&quot; title=&quot;Notes on Self Hosting a Bluesky PDS Alongside Other Services&quot;&gt;PDS-alongside-other-services self-hosting guide&lt;/a&gt; that inspired this post!
&lt;/p&gt;
&lt;p&gt;&lt;picture&gt;&lt;source type=&quot;image/webp&quot; srcset=&quot;https://aitorres.com/img/ozone-login-page.webp 320w, https://aitorres.com/img/ozone-login-page.webp 486w&quot; sizes=&quot;100vw&quot;&gt;&lt;img src=&quot;https://aitorres.com/img/ozone-login-page.png&quot; alt=&quot;Screenshot of the Ozone Bluesky labeler login page&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; width=&quot;486&quot; height=&quot;501&quot; srcset=&quot;https://aitorres.com/img/ozone-login-page.png 320w, https://aitorres.com/img/ozone-login-page.png 486w&quot; sizes=&quot;100vw&quot;&gt;&lt;/picture&gt;&lt;/p&gt;
&lt;figcaption&gt;It&#39;s alive!&lt;/figcaption&gt;
&lt;p&gt;I&#39;ve been tinkering with Bluesky for a few months now through several projects,
and naturally I&#39;ve been drawn towards self-hosting (some of) its pieces, given
Bluesky&#39;s modular and open-source nature.&lt;/p&gt;
&lt;p&gt;My first step was to self-host a
&lt;a href=&quot;https://docs.bsky.app/docs/advanced-guides/entryway&quot;&gt;Personal Data Server (PDS)&lt;/a&gt;,
but the official guide involves deploying a full set of services, including
&lt;code&gt;caddy&lt;/code&gt; as a reverse proxy, running on the host network.&lt;/p&gt;
&lt;p&gt;Since I wanted to re-use an existing server that already had &lt;code&gt;nginx&lt;/code&gt; running, I
followed
&lt;a href=&quot;https://cprimozic.net/notes/posts/notes-on-self-hosting-bluesky-pds-alongside-other-services/&quot;&gt;@ameo.dev&#39;s guide&lt;/a&gt;
to set everything the PDS and, after a few tweaks, I managed to get it working.&lt;/p&gt;
&lt;p&gt;My next step was to self-host &lt;a href=&quot;https://github.com/bluesky-social/ozone&quot;&gt;Ozone&lt;/a&gt;,
Bluesky&#39;s labeling application. Like the PDS instructions, Ozone&#39;s
&lt;a href=&quot;https://github.com/bluesky-social/ozone/blob/main/HOSTING.md&quot;&gt;official hosting guide&lt;/a&gt;
implies running a Docker image in the host network with &lt;code&gt;caddy&lt;/code&gt; and &lt;code&gt;postgres&lt;/code&gt;.
With a bit of work, I adapted the steps to work with an existing &lt;code&gt;nginx&lt;/code&gt; setup
and without interfering with an existing &lt;code&gt;postgres&lt;/code&gt; installation.&lt;/p&gt;
&lt;p&gt;For future reference, here are the tweaks I made to deploy Ozone.&lt;/p&gt;
&lt;h2 id=&quot;start-with-the-official-guide&quot; tabindex=&quot;-1&quot;&gt;Start with the official guide &lt;a class=&quot;header-anchor&quot; href=&quot;https://aitorres.com/blog/self-host-bluesky-ozone-alongside-other-services/#start-with-the-official-guide&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/bluesky-social/ozone/blob/main/HOSTING.md&quot;&gt;Ozone&#39;s self-hosting guide&lt;/a&gt;
is a great starting point to ensure your server has all the prerequisites and
dependencies installed, and enough resources as well.&lt;/p&gt;
&lt;p&gt;You will need the service account for the labeler, the domain pointing to your
server and to open the firewall ports as described.&lt;/p&gt;
&lt;h2 id=&quot;directory-structure-and-config-setup&quot; tabindex=&quot;-1&quot;&gt;Directory structure and config setup &lt;a class=&quot;header-anchor&quot; href=&quot;https://aitorres.com/blog/self-host-bluesky-ozone-alongside-other-services/#directory-structure-and-config-setup&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;I kept the directory structure similar to the one in the official guide, but
instead of creating the base directory in my server&#39;s root, I put it somewhere
else. This is where the data for ozone and the postgres database will live.&lt;/p&gt;
&lt;p&gt;You can skip the &lt;code&gt;caddy&lt;/code&gt; directory since you&#39;re already using &lt;code&gt;nginx&lt;/code&gt;.&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot; tabIndex=&quot;0&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# Navigate to wherever you want to put&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;# ozone data, this is an example&lt;/span&gt;
&lt;span class=&quot;token builtin class-name&quot;&gt;cd&lt;/span&gt; /opt

&lt;span class=&quot;token comment&quot;&gt;# Create the directory structure&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;mkdir&lt;/span&gt; ozone
&lt;span class=&quot;token function&quot;&gt;mkdir&lt;/span&gt; ozone/postgres&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Skip the &lt;code&gt;Caddyfile&lt;/code&gt; as you will not be using &lt;code&gt;caddy&lt;/code&gt; to serve anything.&lt;/p&gt;
&lt;p&gt;Create the postgres config file in the &lt;code&gt;ozone&lt;/code&gt; directory that you created (not
necessarily in &lt;code&gt;/ozone/postgres.env&lt;/code&gt; if you put it somewhere else).&lt;/p&gt;
&lt;p&gt;Then create the &lt;code&gt;ozone.env&lt;/code&gt; file in that directory as well. The only thing to
tweak is the &lt;code&gt;OZONE_DB_POSTGRES_URL&lt;/code&gt; variable. I set it up like this (note that
&lt;code&gt;localhost&lt;/code&gt; was replaced with &lt;code&gt;postgres&lt;/code&gt;):&lt;/p&gt;
&lt;pre class=&quot;language-env&quot; tabIndex=&quot;0&quot;&gt;&lt;code class=&quot;language-env&quot;&gt;OZONE_DB_POSTGRES_URL=postgresql://postgres:${POSTGRES_PASSWORD}@$postgres:5432/ozone&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Since the Docker containers will share the same network (and not the host
network), we can use the Postgres container name as the hostname.&lt;/p&gt;
&lt;h2 id=&quot;ozone-container-setup&quot; tabindex=&quot;-1&quot;&gt;Ozone container setup &lt;a class=&quot;header-anchor&quot; href=&quot;https://aitorres.com/blog/self-host-bluesky-ozone-alongside-other-services/#ozone-container-setup&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;The next step in the official guide would be to download the
&lt;a href=&quot;https://raw.githubusercontent.com/bluesky-social/ozone/main/service/compose.yaml&quot;&gt;&lt;code&gt;compose.yaml&lt;/code&gt;&lt;/a&gt;
file and run it. However, doing this means running everything in the host
network, including &lt;code&gt;caddy&lt;/code&gt;, which we don&#39;t want to do.&lt;/p&gt;
&lt;p&gt;I tweaked the &lt;code&gt;compose.yaml&lt;/code&gt; to look something like this:&lt;/p&gt;
&lt;pre class=&quot;language-yaml&quot; tabIndex=&quot;0&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;networks&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;ozone&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;driver&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; bridge
&lt;span class=&quot;token key atrule&quot;&gt;services&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;ozone&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;container_name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; ozone
    &lt;span class=&quot;token key atrule&quot;&gt;image&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; ghcr.io/bluesky&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;social/ozone&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;0.1&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;restart&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; unless&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;stopped
    &lt;span class=&quot;token key atrule&quot;&gt;networks&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; ozone
    &lt;span class=&quot;token key atrule&quot;&gt;depends_on&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token key atrule&quot;&gt;postgres&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;token key atrule&quot;&gt;condition&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; service_healthy
    &lt;span class=&quot;token key atrule&quot;&gt;ports&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;1235:3000&#39;&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;env_file&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; /opt/ozone/ozone.env
  &lt;span class=&quot;token key atrule&quot;&gt;postgres&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;container_name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; postgres
    &lt;span class=&quot;token key atrule&quot;&gt;image&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; postgres&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;14.11&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;bookworm
    &lt;span class=&quot;token key atrule&quot;&gt;networks&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; ozone
    &lt;span class=&quot;token key atrule&quot;&gt;restart&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; unless&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;stopped
    &lt;span class=&quot;token key atrule&quot;&gt;healthcheck&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token key atrule&quot;&gt;test&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; pg_isready &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;h localhost &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;U $$POSTGRES_USER
      &lt;span class=&quot;token key atrule&quot;&gt;interval&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 2s
      &lt;span class=&quot;token key atrule&quot;&gt;timeout&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 5s
      &lt;span class=&quot;token key atrule&quot;&gt;retries&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;10&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;volumes&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; bind
        &lt;span class=&quot;token key atrule&quot;&gt;source&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; /opt/ozone/postgres
        &lt;span class=&quot;token key atrule&quot;&gt;target&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; /var/lib/postgresql/data
    &lt;span class=&quot;token key atrule&quot;&gt;env_file&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; /opt/ozone/postgres.env&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Note that:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The &lt;code&gt;caddy&lt;/code&gt; and &lt;code&gt;watchtower&lt;/code&gt; containers are gone. A consequence of this is
that you will need to manually update the &lt;code&gt;ozone&lt;/code&gt; container when a new version
is released.&lt;/li&gt;
&lt;li&gt;The paths were adjusted to match the directory structure we created earlier
(in this example, &lt;code&gt;/opt/ozone/&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Containers are set to use &lt;code&gt;ozone&lt;/code&gt; as their bridge network, without being
exposed to the host network.&lt;/li&gt;
&lt;li&gt;Host port &lt;code&gt;1235&lt;/code&gt; is bound to container port &lt;code&gt;3000&lt;/code&gt;, which is the port that
Ozone listens on. You can change this to any other port you want, but make
sure to update the &lt;code&gt;nginx&lt;/code&gt; config accordingly.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Save the file as &lt;code&gt;compose.yaml&lt;/code&gt; and run &lt;code&gt;docker compose up -d&lt;/code&gt; to start the
containers.&lt;/p&gt;
&lt;p&gt;Create, enable and start the &lt;code&gt;systemd&lt;/code&gt; service file as instructed in the
official guide, make sure to set the &lt;code&gt;WorkingDirectory&lt;/code&gt; to wherever you saved
the &lt;code&gt;compose.yaml&lt;/code&gt; file. That takes care of starting &lt;code&gt;ozone&lt;/code&gt; on boot.&lt;/p&gt;
&lt;p&gt;At this point you can verify that Ozone is online by &lt;code&gt;curl&lt;/code&gt;ing
&lt;code&gt;http://localhost:1235/xrpc/_health&lt;/code&gt;. If everything is working, the only step
remaining is to expose the service to the outside world through &lt;code&gt;nginx&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id=&quot;nginx-setup&quot; tabindex=&quot;-1&quot;&gt;Nginx setup &lt;a class=&quot;header-anchor&quot; href=&quot;https://aitorres.com/blog/self-host-bluesky-ozone-alongside-other-services/#nginx-setup&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;I created a new &lt;code&gt;ozone.conf&lt;/code&gt; file within &lt;code&gt;nginx&lt;/code&gt;&#39;s config directory, similar to
this:&lt;/p&gt;
&lt;pre class=&quot;language-nginx&quot; tabIndex=&quot;0&quot;&gt;&lt;code class=&quot;language-nginx&quot;&gt;&lt;span class=&quot;token directive&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;server&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token directive&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;listen&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;80&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token directive&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;server_name&lt;/span&gt; labeler.example.com&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token directive&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;location&lt;/span&gt; /&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token directive&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;proxy_pass&lt;/span&gt; http://localhost:1235&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
      &lt;span class=&quot;token directive&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;proxy_set_header&lt;/span&gt; X-Forwarded-Proto https&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
      &lt;span class=&quot;token directive&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;proxy_set_header&lt;/span&gt; Host &lt;span class=&quot;token variable&quot;&gt;$host&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

      &lt;span class=&quot;token directive&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;proxy_set_header&lt;/span&gt; Upgrade &lt;span class=&quot;token variable&quot;&gt;$http_upgrade&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
      &lt;span class=&quot;token directive&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;proxy_set_header&lt;/span&gt; Connection &lt;span class=&quot;token string&quot;&gt;&quot;Upgrade&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

      &lt;span class=&quot;token directive&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;proxy_buffering&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;off&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Adjust the domain and port as needed, and note that the &lt;code&gt;proxy_set_header&lt;/code&gt; lines
are imporant to ensure websocket connections are properly handled.&lt;/p&gt;
&lt;p&gt;Enable the config and reload &lt;code&gt;nginx&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Then, to add SSL support, use &lt;code&gt;certbot&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot; tabIndex=&quot;0&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;certbot &lt;span class=&quot;token parameter variable&quot;&gt;--nginx&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-d&lt;/span&gt; labeler.example.com&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And you&#39;re set!&lt;/p&gt;
&lt;p&gt;I hope this brief guide helps you set up your own Ozone labeler, and feel free
to &lt;a href=&quot;https://bsky.app/profile/andresitorresm.com&quot;&gt;reach out on Bluesky&lt;/a&gt; if you
have any suggestions for this post!&lt;/p&gt;
</content>
	</entry>
	
	<entry>
		<title>To listen to a book</title>
		<link href="https://aitorres.com/blog/to-listen-to-a-book/"/>
		<updated>2025-05-04T00:00:00Z</updated>
		<id>https://aitorres.com/blog/to-listen-to-a-book/</id>
		<content type="html">&lt;p&gt;&lt;picture&gt;&lt;source type=&quot;image/webp&quot; srcset=&quot;https://aitorres.com/img/reading-the-myth-of-sisyphus.webp 320w, https://aitorres.com/img/reading-the-myth-of-sisyphus.webp 640w, https://aitorres.com/img/reading-the-myth-of-sisyphus.webp 682w&quot; sizes=&quot;100vw&quot;&gt;&lt;source type=&quot;image/png&quot; srcset=&quot;https://aitorres.com/img/reading-the-myth-of-sisyphus.png 320w, https://aitorres.com/img/reading-the-myth-of-sisyphus.png 640w, https://aitorres.com/img/reading-the-myth-of-sisyphus.png 682w&quot; sizes=&quot;100vw&quot;&gt;&lt;img src=&quot;https://aitorres.com/img/reading-the-myth-of-sisyphus.jpeg&quot; alt=&quot;Photo of a Kobo showing the last page of the book &#39;The Myth of Sisyphus&#39;, by Albert Camus&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; width=&quot;682&quot; height=&quot;800&quot; srcset=&quot;https://aitorres.com/img/reading-the-myth-of-sisyphus.jpeg 320w, https://aitorres.com/img/reading-the-myth-of-sisyphus.jpeg 640w, https://aitorres.com/img/reading-the-myth-of-sisyphus.jpeg 682w&quot; sizes=&quot;100vw&quot;&gt;&lt;/picture&gt;&lt;/p&gt;
&lt;figcaption&gt;One of my readings the other day. Trying to imagine Sisyphus happy.&lt;/figcaption&gt;
&lt;p&gt;All my life I have loved reading. Ever since I was a kid, I&#39;ve always kept a
book close by. Although I stopped reading constantly for a few years, I recently
returned to it as a daily habit.&lt;/p&gt;
&lt;p&gt;At first, I read physical books and couldn&#39;t fathom the idea of reading
digitally. Then, I realized that e-books allowed me to carry an entire library
with me wherever I went. So, I embraced digital books.&lt;/p&gt;
&lt;p&gt;However, despite this change, I&#39;d never considered the idea of &lt;em&gt;listening&lt;/em&gt; to a
book. How could I enjoy a story if I didn&#39;t &lt;em&gt;read&lt;/em&gt; it, if I couldn&#39;t see every
symbol, letter, every single decision the author made on a page? This way of
thinking, perhaps shaped a bit by the type of poetry I enjoy (which doesn&#39;t
easily translate from paper to voice), made me reject the idea of audiobooks.&lt;/p&gt;
&lt;p&gt;This year, bit by bit, I&#39;ve opened up to the idea of &lt;em&gt;listening&lt;/em&gt; to &lt;em&gt;read&lt;/em&gt;. I
spent a big part of last year listening to &lt;em&gt;podcasts&lt;/em&gt;, and a few months ago, I
tried my first audiobook, partly thanks to the Vancouver Public Library&#39;s
catalogue.&lt;/p&gt;
&lt;p&gt;Although I still prefer reading on paper or digitally, I haven&#39;t had a bad
experience with audiobooks. I like that I can listen to the story while doing
other mechanical tasks, like cooking or cleaning. And even, since I need to
consciously pay attention to the narrator&#39;s voice, it forces me to concentrate
on the story, to be present in what I&#39;m listening to, instead of getting lost
and drifting between letters and pages.&lt;/p&gt;
&lt;p&gt;I will continue experimenting with different formats, and maybe one day I&#39;ll
incorporate another sense into this quest for knowledge, stories, and worlds. In
the meantime, I&#39;ll split my reading time between digital books and audiobooks.&lt;/p&gt;
</content>
	</entry>
	
	<entry>
		<title>Handling monetary values in code</title>
		<link href="https://aitorres.com/blog/handling-monetary-values-in-code/"/>
		<updated>2025-04-13T00:00:00Z</updated>
		<id>https://aitorres.com/blog/handling-monetary-values-in-code/</id>
		<content type="html">&lt;p&gt;&lt;picture&gt;&lt;source type=&quot;image/webp&quot; srcset=&quot;https://aitorres.com/img/rashed-paykary-OeCT3SgvnK0-unsplash.webp 320w, https://aitorres.com/img/rashed-paykary-OeCT3SgvnK0-unsplash.webp 640w, https://aitorres.com/img/rashed-paykary-OeCT3SgvnK0-unsplash.webp 960w, https://aitorres.com/img/rashed-paykary-OeCT3SgvnK0-unsplash.webp 1280w, https://aitorres.com/img/rashed-paykary-OeCT3SgvnK0-unsplash.webp 1600w, https://aitorres.com/img/rashed-paykary-OeCT3SgvnK0-unsplash.webp 3847w&quot; sizes=&quot;100vw&quot;&gt;&lt;source type=&quot;image/png&quot; srcset=&quot;https://aitorres.com/img/rashed-paykary-OeCT3SgvnK0-unsplash.png 320w, https://aitorres.com/img/rashed-paykary-OeCT3SgvnK0-unsplash.png 640w, https://aitorres.com/img/rashed-paykary-OeCT3SgvnK0-unsplash.png 960w, https://aitorres.com/img/rashed-paykary-OeCT3SgvnK0-unsplash.png 1280w, https://aitorres.com/img/rashed-paykary-OeCT3SgvnK0-unsplash.png 1600w, https://aitorres.com/img/rashed-paykary-OeCT3SgvnK0-unsplash.png 3847w&quot; sizes=&quot;100vw&quot;&gt;&lt;img src=&quot;https://aitorres.com/img/rashed-paykary-OeCT3SgvnK0-unsplash.jpeg&quot; alt=&quot;Photo of a screen showing a close-up of programming code&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; width=&quot;3847&quot; height=&quot;2800&quot; srcset=&quot;https://aitorres.com/img/rashed-paykary-OeCT3SgvnK0-unsplash.jpeg 320w, https://aitorres.com/img/rashed-paykary-OeCT3SgvnK0-unsplash.jpeg 640w, https://aitorres.com/img/rashed-paykary-OeCT3SgvnK0-unsplash.jpeg 960w, https://aitorres.com/img/rashed-paykary-OeCT3SgvnK0-unsplash.jpeg 1280w, https://aitorres.com/img/rashed-paykary-OeCT3SgvnK0-unsplash.jpeg 1600w, https://aitorres.com/img/rashed-paykary-OeCT3SgvnK0-unsplash.jpeg 3847w&quot; sizes=&quot;100vw&quot;&gt;&lt;/picture&gt;&lt;/p&gt;
&lt;figcaption&gt;Photograph by &lt;a href=&quot;https://unsplash.com/@rashedpaykary&quot;&gt;Rashed Paykary&lt;/a&gt; on &lt;a href=&quot;https://unsplash.com/photos/code-and-programming-script-displayed-on-a-computer-screen-OeCT3SgvnK0&quot;&gt;Unsplash&lt;/a&gt;.&lt;/figcaption&gt;
&lt;p&gt;Every developer will eventually have to deal with monetary values in their code
some time. This is a tricky thing to get right, and it is &lt;em&gt;absolutely&lt;/em&gt; necessary
to do so.&lt;/p&gt;
&lt;p&gt;My two cents (no pun intended) on the matter are:&lt;/p&gt;
&lt;h2 id=&quot;dont-reinvent-the-wheel&quot; tabindex=&quot;-1&quot;&gt;Don&#39;t reinvent the wheel &lt;a class=&quot;header-anchor&quot; href=&quot;https://aitorres.com/blog/handling-monetary-values-in-code/#dont-reinvent-the-wheel&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;There are many libraries (e.g. &lt;a href=&quot;https://pypi.org/project/dinero/&quot;&gt;dinero&lt;/a&gt; for
Python) out there that have been built to handle monetary values and their edge
cases. They&#39;re usually tried and tested and will consider a lot of scenarios
that you might not have thought of. Highly recommended!&lt;/p&gt;
&lt;h2 id=&quot;use-the-right-data-type&quot; tabindex=&quot;-1&quot;&gt;Use the right data type &lt;a class=&quot;header-anchor&quot; href=&quot;https://aitorres.com/blog/handling-monetary-values-in-code/#use-the-right-data-type&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;For storage and transport, the right data type is key. Avoid floating point
numbers, as they can introduce rounding errors when adding, substracting or
otherwise making calculations with money.&lt;/p&gt;
&lt;p&gt;Consider using fixed-point numbers (e.g. &lt;code&gt;Decimal&lt;/code&gt; in Python), or integers (e.g.
treat money as cents instead of dollars, like Stripe does). This will help avoid
rounding errors by design.&lt;/p&gt;
&lt;h2 id=&quot;always-consider-localization&quot; tabindex=&quot;-1&quot;&gt;Always consider localization &lt;a class=&quot;header-anchor&quot; href=&quot;https://aitorres.com/blog/handling-monetary-values-in-code/#always-consider-localization&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Different countries and regions have different ways of formatting monetary
values. For example, three dollars and fifty cents would be written as &lt;code&gt;$3.50&lt;/code&gt;
in Canada but &lt;code&gt;$3,50&lt;/code&gt; in France (note the comma instead of the full stop).&lt;/p&gt;
&lt;p&gt;This is &lt;em&gt;particularly&lt;/em&gt; important if you are working on code that sends or
receives monetary values to another system, or that parses monetary values from
user input (e.g. from string). Make sure there is a clear contract between the
two systems or the user input and the code for the format of the monetary
values. Consider how your code will behave if deployed in a different region or
country, and if the system locale can change at runtime.&lt;/p&gt;
&lt;h2 id=&quot;log-everything&quot; tabindex=&quot;-1&quot;&gt;Log everything! &lt;a class=&quot;header-anchor&quot; href=&quot;https://aitorres.com/blog/handling-monetary-values-in-code/#log-everything&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Or, at least, log the most important things to the extend you&#39;re allowed to.
That way you can reconstruct any operation when needed and find out what, if
anything, went wrong (and things &lt;em&gt;will&lt;/em&gt; go wrong).&lt;/p&gt;
&lt;h2 id=&quot;test-test-test&quot; tabindex=&quot;-1&quot;&gt;Test, test, test! &lt;a class=&quot;header-anchor&quot; href=&quot;https://aitorres.com/blog/handling-monetary-values-in-code/#test-test-test&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Finally, test everything thoroughly. Add unit tests for all the testable code,
add integration tests for the components that interact with each other, and
perform end-to-end tests every so often.&lt;/p&gt;
</content>
	</entry>
	
	<entry>
		<title>A new step as an open source project maintainer</title>
		<link href="https://aitorres.com/blog/a-new-step-as-an-open-source-project-maintainer/"/>
		<updated>2025-03-23T00:00:00Z</updated>
		<id>https://aitorres.com/blog/a-new-step-as-an-open-source-project-maintainer/</id>
		<content type="html">&lt;p&gt;&lt;picture&gt;&lt;source type=&quot;image/webp&quot; srcset=&quot;https://aitorres.com/img/code-on-screen-photo.webp 320w, https://aitorres.com/img/code-on-screen-photo.webp 640w, https://aitorres.com/img/code-on-screen-photo.webp 960w, https://aitorres.com/img/code-on-screen-photo.webp 1280w, https://aitorres.com/img/code-on-screen-photo.webp 1600w, https://aitorres.com/img/code-on-screen-photo.webp 1920w&quot; sizes=&quot;100vw&quot;&gt;&lt;source type=&quot;image/png&quot; srcset=&quot;https://aitorres.com/img/code-on-screen-photo.png 320w, https://aitorres.com/img/code-on-screen-photo.png 640w, https://aitorres.com/img/code-on-screen-photo.png 960w, https://aitorres.com/img/code-on-screen-photo.png 1280w, https://aitorres.com/img/code-on-screen-photo.png 1600w, https://aitorres.com/img/code-on-screen-photo.png 1920w&quot; sizes=&quot;100vw&quot;&gt;&lt;img src=&quot;https://aitorres.com/img/code-on-screen-photo.jpeg&quot; alt=&quot;Photo of a screen showing a code editor&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; width=&quot;1920&quot; height=&quot;1282&quot; srcset=&quot;https://aitorres.com/img/code-on-screen-photo.jpeg 320w, https://aitorres.com/img/code-on-screen-photo.jpeg 640w, https://aitorres.com/img/code-on-screen-photo.jpeg 960w, https://aitorres.com/img/code-on-screen-photo.jpeg 1280w, https://aitorres.com/img/code-on-screen-photo.jpeg 1600w, https://aitorres.com/img/code-on-screen-photo.jpeg 1920w&quot; sizes=&quot;100vw&quot;&gt;&lt;/picture&gt;&lt;/p&gt;
&lt;figcaption&gt;Photograph by &lt;a href=&quot;https://unsplash.com/@cdr6934?utm_content=creditCopyText&amp;utm_medium=referral&amp;utm_source=unsplash&quot;&gt;Chris Ried&lt;/a&gt; on &lt;a href=&quot;https://unsplash.com/photos/a-computer-screen-with-a-bunch-of-code-on-it-ieic5Tq8YMk?utm_content=creditCopyText&amp;utm_medium=referral&amp;utm_source=unsplash&quot;&gt;Unsplash&lt;/a&gt;.&lt;/figcaption&gt;
&lt;p&gt;I&#39;ve been working visibly on open source projects for a few years now.&lt;/p&gt;
&lt;p&gt;I started while I was doing my undergrad, participating in the
&lt;a href=&quot;https://hacktoberfest.com/&quot;&gt;Hacktoberfest&lt;/a&gt; in 2019. From there, I moved to
contributing a couple of minor changes to random projects I found on the
internet. At the same time, I worked on my
&lt;a href=&quot;https://github.com/aitorres/caupo&quot;&gt;undergrad thesis&lt;/a&gt; openly on a public Github
repository, as well as on
&lt;a href=&quot;https://github.com/aitorres/firelink&quot;&gt;other big projects&lt;/a&gt; on my final year.&lt;/p&gt;
&lt;p&gt;One of my favorite moments (so far) was when I contributed my first (and, so
far, only) &lt;em&gt;commit&lt;/em&gt; to the &lt;em&gt;mybb&lt;/em&gt; repository, a piece of &lt;em&gt;software&lt;/em&gt; with a
special place in my heart, especially during my teenage years. It wasn&#39;t a big
change, &lt;a href=&quot;https://github.com/mybb/mybb/pull/4392&quot;&gt;just a missing &lt;code&gt;&amp;gt;&lt;/code&gt;&lt;/a&gt;, but it was
another symbolic step for me: being able to give something back to a community
that I held dear.&lt;/p&gt;
&lt;p&gt;I then moved onto creating my own projects. A lot of them didn&#39;t go beyond a
couple commits made in an afternoon or two, and have been left behind. For
others, however, I&#39;ve dedicated more time:
&lt;a href=&quot;https://github.com/aitorres/gutenberg2kindle&quot;&gt;a program to send public domain books to a Kindle via email&lt;/a&gt;,
&lt;a href=&quot;https://github.com/aitorres/oneup&quot;&gt;an assistant to find outdated packages in Python projects&lt;/a&gt;,
&lt;a href=&quot;https://github.com/aitorres/dogpicsbot&quot;&gt;a bot that sends dog pictures in Telegram chats&lt;/a&gt;...
In general, a variety of projects that, for the most part, arose to cover at
least one of the following needs:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Learning how to use a new tool, or&lt;/li&gt;
&lt;li&gt;Solving a personal need&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;For these reasons, and although in recent years I&#39;ve put the same effort into my
personal projects as I have into my professional ones (unit testing, changelogs,
continuous validation, documentation, clean code...), all of them have been a
sort of &lt;em&gt;bottle to the sea&lt;/em&gt;: the exercise of solving my own problems in the
open, with the idea that at some point someone else might find it useful.&lt;/p&gt;
&lt;p&gt;And a couple of weeks ago, that day came.&lt;/p&gt;
&lt;h2 id=&quot;i-received-my-first-feature-request&quot; tabindex=&quot;-1&quot;&gt;I received my first feature request! &lt;a class=&quot;header-anchor&quot; href=&quot;https://aitorres.com/blog/a-new-step-as-an-open-source-project-maintainer/#i-received-my-first-feature-request&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;The other day, a user on Github opened an
&lt;a href=&quot;https://github.com/aitorres/barkr/issues/16&quot;&gt;&lt;em&gt;issue&lt;/em&gt;&lt;/a&gt; in the &lt;em&gt;barkr&lt;/em&gt;
repository, my cross-posting tool for social media, with a request: to add
support for Discord.&lt;/p&gt;
&lt;p&gt;It&#39;s the first time I receive a request of this kind, which makes me think the
following:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;I finally did something that is useful to someone else!&lt;/li&gt;
&lt;li&gt;And not only is it useful to them in its current state, but also in its
future state (to the point of taking the time to write to ask for a change)&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This is most probably an insignificant thing in the grand scheme of things, but
added to the fact that the number of &lt;em&gt;stars&lt;/em&gt; on the Github project has gone up
(and by this I mean: it&#39;s not &lt;em&gt;0&lt;/em&gt;), it made my day, my week, my month, and
reinforced my motivation to keep working on &lt;em&gt;barkr&lt;/em&gt;, improving it little by
little, not just for myself but also (maybe) for someone else.&lt;/p&gt;
&lt;p&gt;How nice it is to do open source! :-)&lt;/p&gt;
</content>
	</entry>
	
	<entry>
		<title>I lost my voice</title>
		<link href="https://aitorres.com/blog/i-lost-my-voice/"/>
		<updated>2025-03-02T00:00:00Z</updated>
		<id>https://aitorres.com/blog/i-lost-my-voice/</id>
		<content type="html">&lt;p class=&quot;post-header-note&quot;&gt;
  This blog post was originally written (and intended to be read) in &lt;a href=&quot;https://aitorres.com/es/blog/perdi-mi-voz/&quot;&gt;Spanish&lt;/a&gt;.
  I tried my best to convey the same meaning in my translation below, but your
  mileage may vary!
&lt;/p&gt;
&lt;p&gt;&lt;picture&gt;&lt;source type=&quot;image/webp&quot; srcset=&quot;https://aitorres.com/img/writing-mac-phone-notebook.webp 320w, https://aitorres.com/img/writing-mac-phone-notebook.webp 640w&quot; sizes=&quot;100vw&quot;&gt;&lt;source type=&quot;image/png&quot; srcset=&quot;https://aitorres.com/img/writing-mac-phone-notebook.png 320w, https://aitorres.com/img/writing-mac-phone-notebook.png 640w&quot; sizes=&quot;100vw&quot;&gt;&lt;img src=&quot;https://aitorres.com/img/writing-mac-phone-notebook.jpeg&quot; alt=&quot;Picture of a desk. On top of it, there&#39;s a Macbook, a coffee mug, a phone and a notebook with a pen on top.&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; width=&quot;640&quot; height=&quot;427&quot; srcset=&quot;https://aitorres.com/img/writing-mac-phone-notebook.jpeg 320w, https://aitorres.com/img/writing-mac-phone-notebook.jpeg 640w&quot; sizes=&quot;100vw&quot;&gt;&lt;/picture&gt;&lt;/p&gt;
&lt;figcaption&gt;Photograph by &lt;a href=&quot;https://unsplash.com/@andrewtneel?utm_content=creditCopyText&amp;utm_medium=referral&amp;utm_source=unsplash&quot;&gt;Andrew Neel&lt;/a&gt; on &lt;a href=&quot;https://unsplash.com/photos/macbook-pro-white-ceramic-mugand-black-smartphone-on-table-cckf4TsHAuw?utm_content=creditCopyText&amp;utm_medium=referral&amp;utm_source=unsplash&quot;&gt;Unsplash&lt;/a&gt;.&lt;/figcaption&gt;
&lt;p&gt;For a while, so long that I can&#39;t measure, I&#39;ve felt like my voice is missing.&lt;/p&gt;
&lt;p&gt;I don&#39;t know where I left it last time I saw it, I used it.&lt;/p&gt;
&lt;p&gt;My fingers dance atop the keyboard, my hand grabs the pen that swipes over the
notebook. And I can see the lines, the traces,the pixels that light up forming
symbols that I can read.&lt;/p&gt;
&lt;p&gt;But I don&#39;t recognize myself.&lt;/p&gt;
&lt;hr class=&quot;asterisk-divider&quot;&gt;
&lt;p&gt;For the past few months, I&#39;ve consciously grown back my reading habit, partially
due to
&lt;a href=&quot;https://aitorres.com/blog/getting-a-kobo-rekindled-my-passion-for-reading/&quot;&gt;me getting a new Kobo&lt;/a&gt;.
When I created this website, I set another goal for myself: to recover my
writing habit. But this one&#39;s been way harder to do.&lt;/p&gt;
&lt;p&gt;Even though, objectivelly, I&#39;ve been &lt;em&gt;practicing&lt;/em&gt; more than before —I&#39;ve forced
myself to write at least a couple paragraphs each day, about anything, like the
hamster that comes back to the wheel that he&#39;s used to—, I don&#39;t feel completely
comfortable with my output.&lt;/p&gt;
&lt;p&gt;And, to be fair, I&#39;ve never been. Not completely. But even within the walls of
my self-doubt, I could find parts of myself engraved within the texts that I
used to write years ago.&lt;/p&gt;
&lt;p&gt;Now, if I put my words in front of a mirror, it is not me whom I see.&lt;/p&gt;
&lt;hr class=&quot;asterisk-divider&quot;&gt;
&lt;p&gt;I find writing to be &lt;em&gt;essential&lt;/em&gt; to my identity. I like recognizing myself as a
&lt;em&gt;writer&lt;/em&gt;, but my writing these past few years has not met my standards by far.&lt;/p&gt;
&lt;p&gt;I&#39;m not sure if it&#39;s because of the time that has passed, or the immense change
in my circumstances and environment since 2023 —starting with the obvious:
migrating to a new country, leaving behind the Caribbean sand and instead
finding myself trapped within the white confines of the snowy mountains that I
can see from my windows.&lt;/p&gt;
&lt;p&gt;Something is not working.&lt;/p&gt;
&lt;p&gt;But within this self-disagreement, I also find a wish: to improve, to be better,
and to go back and find myself again within the role of a writer.&lt;/p&gt;
&lt;p&gt;I will find the way, I will find the time.&lt;/p&gt;
&lt;p&gt;In the meantime, I will continue pursuing my identity as a writer, just as much
as I pursue all the other bits of whom I am.&lt;/p&gt;
&lt;p&gt;And I will keep finding myself in the role of a reader, surfing endless
reminders of all that&#39;s possible, of the thousands of realities that we can
live, that we can buid, that we can create.&lt;/p&gt;
</content>
	</entry>
	
	<entry>
		<title>Why I don&#39;t bother with self-help books</title>
		<link href="https://aitorres.com/blog/why-i-dont-bother-with-self-help-books/"/>
		<updated>2025-02-02T00:00:00Z</updated>
		<id>https://aitorres.com/blog/why-i-dont-bother-with-self-help-books/</id>
		<content type="html">&lt;p&gt;A few days ago, as I was cooking (and mindlessly watching YouTube in the
background), a book recommendation video caught my eye. A YouTuber was
describing her experience reading &amp;quot;The Dip&amp;quot; (Seth Godin, 2007), and how the
message behind this book had pushed her into accomplishing several milestones in
her path as a content creator and businesswoman.&lt;/p&gt;
&lt;p&gt;I&#39;ve never been fond of self-help books, but I gave &amp;quot;The Dip&amp;quot; a try. It was a
quick read, I read it in one sitting during a lunch break.&lt;/p&gt;
&lt;p&gt;And it reminded me of why I don&#39;t really like self-help books.&lt;/p&gt;
&lt;p&gt;Now, I know that 1) no two books are the same, even if they&#39;re sold / promoted
as part of the same genre, and 2) no two readers are the same, so what doesn&#39;t
work for me can potentially work for someone else and that&#39;s alright.&lt;/p&gt;
&lt;p&gt;However, if you ask me, self-help books fail to reach me because:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;They&#39;re usually overly simplistic. Self-help authors tend to reduce problems
and situations into one box with a fixed outcome. Everything tends to be
black or white.&lt;/li&gt;
&lt;li&gt;They&#39;re deterministic. The authors ignore that everyone&#39;s circumstances are
different and aim to provide a rigid formula for success (in business, in
love, in finances, you name it). Do what they say and you&#39;ll succeed. Don&#39;t,
and you&#39;ll fail.&lt;/li&gt;
&lt;li&gt;They&#39;re pompous common sense. Most of the messages and guidance are very
basic, rarely new advice, and they&#39;re inflated with fancy words, terms,
charts to make it look like they&#39;re new or revolutionary. Granted, common
sense isn&#39;t actually common and sometimes a reminder is fine, but still...&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Maybe this has to do with the tendency to attempt to turn &amp;quot;success&amp;quot; into a
process on extremely subjective areas. We want to have an easy-to-follow guide,
a &amp;quot;follow these steps and you&#39;ll do it without fail&amp;quot;, but life doesn&#39;t usually
work that way.&lt;/p&gt;
&lt;p&gt;This being said, &amp;quot;The Dip&amp;quot;&#39;s message (that can be shortened from 80-pages to one
or two lines) was a curious, albeit simplistic, premise. I&#39;m not sure if it&#39;ll
change what I do or how I act, but I don&#39;t regret reading it.&lt;/p&gt;
&lt;p&gt;At least I got a blog post out of it ;)&lt;/p&gt;
</content>
	</entry>
	
	<entry>
		<title>Some notes after DMing my first Dungeons and Dragons campaign</title>
		<link href="https://aitorres.com/blog/notes-after-dming-my-first-dungeons-and-dragons-campaign/"/>
		<updated>2025-01-27T00:00:00Z</updated>
		<id>https://aitorres.com/blog/notes-after-dming-my-first-dungeons-and-dragons-campaign/</id>
		<content type="html">&lt;p&gt;&lt;picture&gt;&lt;source type=&quot;image/webp&quot; srcset=&quot;https://aitorres.com/img/dragons-of-stormwreck-isle-details.webp 320w, https://aitorres.com/img/dragons-of-stormwreck-isle-details.webp 640w&quot; sizes=&quot;100vw&quot;&gt;&lt;img src=&quot;https://aitorres.com/img/dragons-of-stormwreck-isle-details.png&quot; alt=&quot;A promotional image for the Dragons of Stormwreck Isle campaign (Dungeons and Dragons)&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; width=&quot;640&quot; height=&quot;640&quot; srcset=&quot;https://aitorres.com/img/dragons-of-stormwreck-isle-details.png 320w, https://aitorres.com/img/dragons-of-stormwreck-isle-details.png 640w&quot; sizes=&quot;100vw&quot;&gt;&lt;/picture&gt;&lt;/p&gt;
&lt;figcaption&gt;Facing a dragon for the first time! (image: Wizards of the Coast)&lt;/figcaption&gt;
&lt;p&gt;Between October and December, I took on the role of Dungeon Master (DM) for the
first time to host a short (lv. 1 to 3) introductory Dungeons and Dragons
campaign for two close friends, Germán and Gustavo.&lt;/p&gt;
&lt;p&gt;This was not only my first time as a DM, but my first real taste of a Dungeons
and Dragons game overall, as my only other time playing was on a 2-hour one-shot
tutorial a few weeks before. But the game felt so creative and attractive to me
that, after a few failed attempts to find more experienced players to guide our
first campaign, I read through the whole campaign and most of the Dungeon
Masters Guide and hosted it myself.&lt;/p&gt;
&lt;p&gt;We had a lot of fun! Besides the source materials, my only other preparation was
watching countless YouTube videos with tips and tricks for new DMs, and browsing
some &lt;em&gt;reddit&lt;/em&gt; posts for campaign-specific tips. I was nervous at first, but
thanks to my friends getting into their roles, each session felt rewarding and
fun. It helped that we chose to start with the current Starter Kit, &lt;strong&gt;Dragons of
Stormwreck Isle&lt;/strong&gt;, which is perfect for newcomes to Dungeons and Dragons.&lt;/p&gt;
&lt;p&gt;Now that the campaign is over and about a month has passed, I&#39;ve had some time
to look back and think of a few extra tips that I will take into account for my
next campaign as a DM (and that I surely read somewhere but completely
ignored!).&lt;/p&gt;
&lt;p&gt;In no particular order:&lt;/p&gt;
&lt;h3 id=&quot;notes-notes-and-more-notes&quot; tabindex=&quot;-1&quot;&gt;Notes, notes and more notes &lt;a class=&quot;header-anchor&quot; href=&quot;https://aitorres.com/blog/notes-after-dming-my-first-dungeons-and-dragons-campaign/#notes-notes-and-more-notes&quot;&gt;#&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Being a DM means improvising a lot: creating NPCs on the fly, addressing
backstory or plot holes with the first thing that comes to mind, remembering
whether or not you already mentioned that the statue next to the room is
shining...&lt;/p&gt;
&lt;p&gt;It&#39;s important to actively take notes of what you do, where you&#39;re at, what&#39;s
your next step and what your players have done. You can involve your players in
your note-taking process by encouraging them to do so and share important notes
when a session ends and / or before a session starts.&lt;/p&gt;
&lt;h3 id=&quot;be-ready-for-the-unexpected&quot; tabindex=&quot;-1&quot;&gt;Be ready for the unexpected &lt;a class=&quot;header-anchor&quot; href=&quot;https://aitorres.com/blog/notes-after-dming-my-first-dungeons-and-dragons-campaign/#be-ready-for-the-unexpected&quot;&gt;#&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;You can plan a whole session-long storyline, and the first thing your players do
is decide they want to do something else. It&#39;s impossible to be prepared for
&lt;em&gt;every possible outcome&lt;/em&gt;, but you should help your future self by at least
considering what other actions could your players do if they decide they don&#39;t
want to pursue your main story plot just yet.&lt;/p&gt;
&lt;p&gt;For example, you can have some campaign-appropriate creatures and maps ready so
that, if they adventure somewhere new, you&#39;re ready to add an unplanned
encounter. This can even help you figure out how to tie what your players are
doing with your original ideas!&lt;/p&gt;
&lt;h3 id=&quot;help-control-the-course-of-action&quot; tabindex=&quot;-1&quot;&gt;Help control the course of action &lt;a class=&quot;header-anchor&quot; href=&quot;https://aitorres.com/blog/notes-after-dming-my-first-dungeons-and-dragons-campaign/#help-control-the-course-of-action&quot;&gt;#&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Tied to my point above: a Dungeons and Dragons campaign is a storytelling
exercise where your players take decisions to drive the story forward, but they
do so based on your guidance as a Dungeon Master.&lt;/p&gt;
&lt;p&gt;Don&#39;t be afraid to suggest different ways to solve an incident or address an
encounter, to help the players understand all the possibilities they have
without taking agency from them.&lt;/p&gt;
&lt;h3 id=&quot;players-dont-know-what-you-know-so-help-them-see&quot; tabindex=&quot;-1&quot;&gt;Players don&#39;t know what you know, so help them see &lt;a class=&quot;header-anchor&quot; href=&quot;https://aitorres.com/blog/notes-after-dming-my-first-dungeons-and-dragons-campaign/#players-dont-know-what-you-know-so-help-them-see&quot;&gt;#&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;If there&#39;s a suspicious-looking plank inside a room, say so!&lt;/p&gt;
&lt;p&gt;If there&#39;s five columns but one seems to shine, say so!&lt;/p&gt;
&lt;p&gt;If there&#39;s some mechanism that depends on the players taking a given action,
find a way to let them know without throwing the fact into their faces. Invite
them to examine or investigate, or be more detailed describing the things you
want to draw them upon.&lt;/p&gt;
&lt;p&gt;New players might not have the impulse to investigate every place they find
themselves into, so try to bring awareness to what they &lt;em&gt;could&lt;/em&gt; do in those
places.&lt;/p&gt;
&lt;h3 id=&quot;remember-to-have-fun&quot; tabindex=&quot;-1&quot;&gt;Remember to have fun &lt;a class=&quot;header-anchor&quot; href=&quot;https://aitorres.com/blog/notes-after-dming-my-first-dungeons-and-dragons-campaign/#remember-to-have-fun&quot;&gt;#&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Preparing and hosting a session can be stressful. There&#39;s a lot of work
behind-the-scenes, calculations on the go, plans that can change every second...
But remember to have fun! If your players and yourself are having fun, that&#39;s
all that matters.&lt;/p&gt;
</content>
	</entry>
	
	<entry>
		<title>Port forwarding through SSH</title>
		<link href="https://aitorres.com/blog/port-forwarding-through-ssh/"/>
		<updated>2025-01-19T00:00:00Z</updated>
		<id>https://aitorres.com/blog/port-forwarding-through-ssh/</id>
		<content type="html">&lt;p&gt;While working on my undergraduate senior thesis a few years ago, I made a small
web application to visualize certain patterns as my daily experiments went
through. This application was running inside a remote server and was not getting
exposed to the public internet, so it was a hassle to check its content.&lt;/p&gt;
&lt;p&gt;After trying to figure out a way to make it easier to access said application, I
stumbled upon the port forwarding functionality of &lt;code&gt;ssh&lt;/code&gt;. I&#39;m writing this brief
explanation many years later to prevent myself from forgetting it again.&lt;/p&gt;
&lt;p&gt;As per &lt;a href=&quot;https://linuxcommand.org/lc3_man_pages/ssh1.html&quot;&gt;ssh&#39;s man page&lt;/a&gt;, you
can use the &lt;code&gt;-L&lt;/code&gt; flag to specify &amp;quot;that connections to the given TCP port or Unix
socket on the local (client) host are to be forwarded to the given host and
port, or Unix socket, on the remote side&amp;quot;.&lt;/p&gt;
&lt;p&gt;Practically speaking, if your remote server is located at &lt;code&gt;example.com&lt;/code&gt; and is
running an application that listens for connections on port &lt;code&gt;8001&lt;/code&gt;, you can run
this command on your local machine:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot; tabIndex=&quot;0&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;ssh&lt;/span&gt; user@example.com &lt;span class=&quot;token parameter variable&quot;&gt;-L&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;3000&lt;/span&gt;:localhost:8001&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You can read the &lt;code&gt;3000:localhost:8001&lt;/code&gt; part as &amp;quot;connections to (my local) port
&lt;code&gt;3000&lt;/code&gt; is to be forwarded to the remote server, to the &lt;code&gt;localhost&lt;/code&gt; host and the
&lt;code&gt;8001&lt;/code&gt; port&amp;quot;. Adjust the ports and host to suit your use case.&lt;/p&gt;
&lt;p&gt;While this &lt;code&gt;ssh&lt;/code&gt; session is running, you can access &lt;code&gt;localhost:3000&lt;/code&gt; on your
local machine and you should be greeted by whatever is available on
&lt;code&gt;localhost:8001&lt;/code&gt; on the remote machine. Nice!&lt;/p&gt;
</content>
	</entry>
	
	<entry>
		<title>I love building bots (and bot craftmanship)</title>
		<link href="https://aitorres.com/blog/i-love-building-bots/"/>
		<updated>2024-12-19T00:00:00Z</updated>
		<id>https://aitorres.com/blog/i-love-building-bots/</id>
		<content type="html">&lt;p&gt;A few months ago, I received notice that the
&lt;a href=&quot;https://muffinlabs.com/posts/2024/10/29/10-29-rip-botsin-space/&quot;&gt;botsin.space Mastodon instance&lt;/a&gt;
was going to shut down by December this year, after more than 7 years of
service. I hope bot-makers are able to migrate their bots to other instances and
keep them running, to keep the online bot craftmanship alive.&lt;/p&gt;
&lt;p&gt;While I only found my way into the fediverse a couple years back, I&#39;ve loved
making online bots (e.g. on Telegram, formerly on X (formerly Twitter)) and also
stumbling upon these small creations made by other people ever since I started
college more than ten years ago.&lt;/p&gt;
&lt;p&gt;These toy bots are by no means a technological feat: most of the ones I refer to
just post short pieces of text to social media, or respond to triggers on
instant messaging apps. But no matter how advanced they are, I find it wonderful
to connect to a bot creator and understand the creative intent behind the bot.&lt;/p&gt;
&lt;p&gt;I remember a few bots that inspired me to start creating my own: among others,
there was a bot on Twitter that posted imaginary news headlines for a fictional
RPG setting, and another one that posted ASCII-art of small boats sailing
through the nights. There&#39;s also a lot of bots that post excerpts of poets and
short stories, and the more useful variety (weather, real headlines, earthquake
warnings, etc).&lt;/p&gt;
&lt;p&gt;Due to the &lt;em&gt;botsin.space&lt;/em&gt; closure, I had to migrate one of my bots
(&lt;a href=&quot;https://aitorres.com/projects/caracas-adjetivo/&quot;&gt;@caracasadjetivo&lt;/a&gt;) to another instance.
Originally on Twitter, I decided to move it to a self-hosted, barebones instance
at &lt;a href=&quot;https://caracasadj.aitorres.com&quot;&gt;caracasadj.aitorres.com&lt;/a&gt;, made possible by
&lt;a href=&quot;https://gitlab.com/edent/activity-bot/&quot;&gt;ActivityBot&lt;/a&gt;. While I&#39;ve had a few
other bots in the past (a weather bot, a few poetry-related bots...), this is
the only one I currently keep active. That might change in the (near) future, as
I go back to my old files and re-deploy other bots in the fediverse.&lt;/p&gt;
&lt;p&gt;I don&#39;t really have a bottom line or conclusion here, other than inviting you to
explore small-sized social interactions on Mastodon or other similar places
through the creation of bots. You don&#39;t need AI, or LLMs, or any fancy
technology for that. Just a few lines of code, an idea and a spark to bring it
to life.&lt;/p&gt;
&lt;p&gt;Happy botting :-)&lt;/p&gt;
</content>
	</entry>
	
	<entry>
		<title>Building a game out of spite</title>
		<link href="https://aitorres.com/blog/building-a-game-out-of-spite/"/>
		<updated>2024-11-10T00:00:00Z</updated>
		<id>https://aitorres.com/blog/building-a-game-out-of-spite/</id>
		<content type="html">&lt;p&gt;So, I just built a (very simple) web game: &lt;a href=&quot;https://aitorres.com/projects/number-sum/&quot;&gt;Number Sum&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The game is a brief number guessing game, in the same vein as Sudoku and others.
You have to complete a partially-filled grid with number guesses to ensure that
certain conditions are met, namely that all columns and rows sum to the
respective hints given on the grid.&lt;/p&gt;
&lt;p&gt;It took me about two hours to build (plus a few weeks of &lt;em&gt;wanting&lt;/em&gt; to build it
but not moving a finger), and I did for two reasons:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;The main, technical reason is that I wanted to practice my Svelte skills.
I&#39;ve been eyeing out Svelte for some time and this seemed like a perfect
opportunity to build a small web app encapsulating scripts, styles and markup
in the same file. Plus, I don&#39;t practice my Javascript / Typescript as much
as I would like to on my day-to-day job.&lt;/li&gt;
&lt;li&gt;I played a similar game on my phone &lt;strong&gt;and I hated it&lt;/strong&gt;. It was one of these
games full of obnoxious, intrusive ads that you can&#39;t skip and that pop out
at every. single. opportunity.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;So thanks to these two reasons (mostly the second one, to be honest), you can
now play an ad-free, open source version of the game!&lt;/p&gt;
&lt;p&gt;Now, to work on &lt;em&gt;actually&lt;/em&gt; productive things...&lt;/p&gt;
</content>
	</entry>
	
	<entry>
		<title>Getting a Kobo rekindled my passion for reading</title>
		<link href="https://aitorres.com/blog/getting-a-kobo-rekindled-my-passion-for-reading/"/>
		<updated>2024-11-08T00:00:00Z</updated>
		<id>https://aitorres.com/blog/getting-a-kobo-rekindled-my-passion-for-reading/</id>
		<content type="html">&lt;p&gt;When I was younger, I used to read a lot. I used to cram in reading at every
possible moment, dashing through trilogies and series in a matter of days. I
would consistently start reading a novel, a short story collection, a series of
essays and get through the end without getting bored or tired in the process,
even if whatever book I had picked didn&#39;t fully peak my interest. Physical
books, ebooks, fan fiction, blog posts, RSS feeds: you name it, I probably read
it.&lt;/p&gt;
&lt;p&gt;And then I grew into adulthood.&lt;/p&gt;
&lt;p&gt;At some point after my first year of undergraduate studies, I stopped reading as
much as I used to. More and more of what I read was content I &lt;em&gt;had&lt;/em&gt; to read:
textbooks, problem sets, required reading for my courses; not that I didn&#39;t like
what I was reading (most of the time at least), but &lt;em&gt;mandatory&lt;/em&gt; reading
certainly overpowered &lt;em&gt;recreative&lt;/em&gt; reading to the point that I could spend
months without even opening a book.&lt;/p&gt;
&lt;p&gt;&lt;picture&gt;&lt;source type=&quot;image/webp&quot; srcset=&quot;https://aitorres.com/img/goodreads-reading-stats.webp 320w, https://aitorres.com/img/goodreads-reading-stats.webp 640w, https://aitorres.com/img/goodreads-reading-stats.webp 960w, https://aitorres.com/img/goodreads-reading-stats.webp 986w&quot; sizes=&quot;100vw&quot;&gt;&lt;img src=&quot;https://aitorres.com/img/goodreads-reading-stats.png&quot; alt=&quot;A screenshot from the Goodreads application showcasing my books read per year.&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; width=&quot;986&quot; height=&quot;897&quot; srcset=&quot;https://aitorres.com/img/goodreads-reading-stats.png 320w, https://aitorres.com/img/goodreads-reading-stats.png 640w, https://aitorres.com/img/goodreads-reading-stats.png 960w, https://aitorres.com/img/goodreads-reading-stats.png 986w&quot; sizes=&quot;100vw&quot;&gt;&lt;/picture&gt;&lt;/p&gt;
&lt;figcaption&gt;While these Goodreads stats are not the most accurate as I don&#39;t track everything that I read, they serve to illustrate my point.&lt;/figcaption&gt;
&lt;p&gt;A &lt;em&gt;certain dystopic event that marked the start of the 2020s&lt;/em&gt; (sigh) gave me a
lot of free time trapped inside a house that I shared with eight other friends,
so in order to &lt;em&gt;not&lt;/em&gt; go crazy, I decided to turn back into my passions and
hobbies, and in the case of reading I made an investment that initially proved
fruitful.&lt;/p&gt;
&lt;p&gt;I bought a Kindle.&lt;/p&gt;
&lt;p&gt;&lt;picture&gt;&lt;source type=&quot;image/webp&quot; srcset=&quot;https://aitorres.com/img/my-kindle.webp 320w, https://aitorres.com/img/my-kindle.webp 617w&quot; sizes=&quot;100vw&quot;&gt;&lt;source type=&quot;image/png&quot; srcset=&quot;https://aitorres.com/img/my-kindle.png 320w, https://aitorres.com/img/my-kindle.png 617w&quot; sizes=&quot;100vw&quot;&gt;&lt;img src=&quot;https://aitorres.com/img/my-kindle.jpeg&quot; alt=&quot;A picture of my Kindle Paperwhite (2019 model) taken in 2024.&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; width=&quot;617&quot; height=&quot;759&quot; srcset=&quot;https://aitorres.com/img/my-kindle.jpeg 320w, https://aitorres.com/img/my-kindle.jpeg 617w&quot; sizes=&quot;100vw&quot;&gt;&lt;/picture&gt;&lt;/p&gt;
&lt;figcaption&gt;Earlier this year, taking my Kindle for a walk along the Vancouver seawall to catch-up on a Spanish translation of &#39;Gender Trouble&#39; by Judith Butler&lt;/figcaption&gt;
&lt;p&gt;Initially, the Kindle did wonders for my reading habits. In 2020 alone I read 28
books according to my Goodreads stats, which is 25 more books than the previous
year (!!). I felt pushed into the future by the usual wonders of an e-reader:
its practicality, being able to carry a sizable collection of books in my
pocket, not having to choose which book to carry with me on a walk or when going
to a coffee shop, etc.&lt;/p&gt;
&lt;p&gt;But, once again, I found myself reading less and less. In 2021 I read 15 less
books than the year before, and the number dropped to just two books over all
of 2022.&lt;/p&gt;
&lt;p&gt;Earlier this year, after finally settling from an international move and a new
job with lots of things that were new to me, I decided to re-engage with my
passions, pushed in part by the warm and welcoming literary scene in Vancouver.
Armed with library cards for the different libraries of Metro Vancouver, I
started checking books out and working on my reading once again.&lt;/p&gt;
&lt;p&gt;While partially successful, I got caught in the same issues that I used to avoid
by reading digital: either I forgot the book I was reading at home, or took the
wrong one on my trips and halfway through an hour long commute decided that I
actually wanted to read something else.&lt;/p&gt;
&lt;p&gt;I then tried getting back into my Kindle, but for some reason, I started
noticing some drawbacks from the model I had. My Kindle Paperwhite (2019) felt
sluggish whenever I tried to navigate its UI, and changing pages took long
enough to break my immersion as a reader. I assumed it was a fault of e-readers
in general, of the eInk technology or something similar.&lt;/p&gt;
&lt;p&gt;And then I stumbled upon a Kobo stand in an Indigo bookstore.&lt;/p&gt;
&lt;p&gt;Up until this point, I knew there were other e-reader manufacturers besides
Amazon, but I had assumed that Amazong, by virtue of being the biggest name, had
the best technology. This stopped making sense once I started playing around
with the different Kobo models on display at the bookstore. I found out that
there were color e-readers! This was completely new to me! However, as I don&#39;t
usually read anything that&#39;s not in black and white, I shifted my attention to
the other models.&lt;/p&gt;
&lt;p&gt;The Kobo Clara BW caught my attention. I liked its small form factor, similar to
the Kindle that I had already grown used to. I liked that it didn&#39;t have any
buttons (contrary to what is apparently the popular belief, I&#39;m more fond of
touching the screen to change to the next page). But what really sold it for me
was the page change speed. It was years ahead of my Kindle. Everything felt
extremely smooth, and in a snap of the fingers I could move from a page to the
next to the next to the next.&lt;/p&gt;
&lt;p&gt;I went to YouTube and Reddit, watched and read reviews and articles and
comparisons, went to the Indigo bookstore a dozen more times to test the
devices, meditated over a few more weeks and finally I did it.&lt;/p&gt;
&lt;p&gt;I bought a Kobo.&lt;/p&gt;
&lt;p&gt;&lt;picture&gt;&lt;source type=&quot;image/webp&quot; srcset=&quot;https://aitorres.com/img/my-kobo.webp 320w, https://aitorres.com/img/my-kobo.webp 640w, https://aitorres.com/img/my-kobo.webp 939w&quot; sizes=&quot;100vw&quot;&gt;&lt;img src=&quot;https://aitorres.com/img/my-kobo.png&quot; alt=&quot;A picture of my Kobo Clara BW box being held by a bear plushie, taken in 2024.&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; width=&quot;939&quot; height=&quot;1252&quot; srcset=&quot;https://aitorres.com/img/my-kobo.png 320w, https://aitorres.com/img/my-kobo.png 640w, https://aitorres.com/img/my-kobo.png 939w&quot; sizes=&quot;100vw&quot;&gt;&lt;/picture&gt;&lt;/p&gt;
&lt;figcaption&gt;Behold, the Kobo Clara BW! Thanks to &#39;Malosito&#39; for holding the box while I took the picture.&lt;/figcaption&gt;
&lt;p&gt;After almost two months of being a Kobo owner, I can happily say that it has
served a purpose in increasing the time I dedicate to reading. Besides the fast
page turning speed, I also love that I can link my Kobo to my library account
(which is not possible for Canadian residents using a Kindle). This lets me
access my library&#39;s more than eight thousand ebooks and manage my holds and
loans directly from my device.&lt;/p&gt;
&lt;p&gt;I&#39;ve used my Kobo to read poetry, short fiction, essays and manga so far, and
it&#39;s outperformed the Kindle in all areas. That&#39;s why, if you go back to the
first image of this post, the year 2024 is the year I&#39;ve read more books in
Goodreads (granted, most of those are manga volumes, but it&#39;s still a lot of
pages to turn and read!).&lt;/p&gt;
&lt;p&gt;&lt;picture&gt;&lt;source type=&quot;image/webp&quot; srcset=&quot;https://aitorres.com/img/kobo-in-train.webp 320w, https://aitorres.com/img/kobo-in-train.webp 640w, https://aitorres.com/img/kobo-in-train.webp 687w&quot; sizes=&quot;100vw&quot;&gt;&lt;img src=&quot;https://aitorres.com/img/kobo-in-train.png&quot; alt=&quot;A picture of my Kobo Clara BW while I read on a train.&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; width=&quot;687&quot; height=&quot;829&quot; srcset=&quot;https://aitorres.com/img/kobo-in-train.png 320w, https://aitorres.com/img/kobo-in-train.png 640w, https://aitorres.com/img/kobo-in-train.png 687w&quot; sizes=&quot;100vw&quot;&gt;&lt;/picture&gt;&lt;/p&gt;
&lt;figcaption&gt;Reading some manga on the Kobo Clara BW on a five-hour long train trip.&lt;/figcaption&gt;
&lt;p&gt;Indirectly, just having the &lt;em&gt;ability&lt;/em&gt; to read in a comfortable way that doesn&#39;t
break immersion, and also to procure books so easily thanks to the link to my
library, has motivated me to get organized and do more reading. I&#39;ve gone back
to reading literary blogs to find recommendations, I&#39;ve taken part of literature
events and creative writing classes and workshops, I&#39;ve written down notes on
what I am currently reading.&lt;/p&gt;
&lt;p&gt;In general, I am enjoying consuming books again, and I don&#39;t have to &lt;em&gt;force&lt;/em&gt;
myself to read anymore. Instead, I crave finishing all my other tasks of the day
as early as possible to dive into my Kobo and pick up my stories where I last
left them. This has also helped reduce my doomscrolling time (although, I won&#39;t
lie, I still lose time every day wandering the backdoors of TikTok and the
like).&lt;/p&gt;
&lt;p&gt;So, all things considered, I&#39;m happy with my Kobo. So long, Kindle, you have
been dethroned!&lt;/p&gt;
</content>
	</entry>
	
	<entry>
		<title>Block malicious user agents via nginx config</title>
		<link href="https://aitorres.com/blog/block-user-agents-on-nginx-config/"/>
		<updated>2024-10-05T00:00:00Z</updated>
		<id>https://aitorres.com/blog/block-user-agents-on-nginx-config/</id>
		<content type="html">&lt;p&gt;Bots are everywhere, and so are malicious agents. They crawl, they scrape, they
read... and sometimes (or most? of the times),they &lt;em&gt;abuse&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;A few times now, I&#39;ve observed increased CPU / RAM consumption on servers where
I wouldn&#39;t expect any usage on non-peak hours, and after browsing through access
logs, I find out the cause is that certain bots are crawling my sites over and
over again.&lt;/p&gt;
&lt;p&gt;This has been made worse, for example, by certain WordPress plug-ins that
generate links with unique GET parameters on each page load, making these
not-so-smart crawler bots get stuck on an endless loop. What a waste of
bandwidth and compute resources.&lt;/p&gt;
&lt;p&gt;To put an end to this, I&#39;ve now blocked by default certain user agents from
accessing any of the websites I host, directly on &lt;code&gt;nginx&lt;/code&gt; config. This ensures
that malicious actors are stopped even before they can trigger any server
execution or load static content. You can follow similar steps or even swap user
agent for any other identifier you define to block agents from accessing your
servers.&lt;/p&gt;
&lt;h2 id=&quot;create-a-list-of-blocked-user-agents&quot; tabindex=&quot;-1&quot;&gt;Create a list of &lt;em&gt;blocked&lt;/em&gt; user agents &lt;a class=&quot;header-anchor&quot; href=&quot;https://aitorres.com/blog/block-user-agents-on-nginx-config/#create-a-list-of-blocked-user-agents&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Create a new file in &lt;code&gt;/etc/nginx&lt;/code&gt; with the following content and any name you
want.&lt;/p&gt;
&lt;p&gt;I&#39;ll use &lt;code&gt;blocked_user_agent.rules&lt;/code&gt; here:&lt;/p&gt;
&lt;pre class=&quot;language-nginx&quot; tabIndex=&quot;0&quot;&gt;&lt;code class=&quot;language-nginx&quot;&gt;&lt;span class=&quot;token directive&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;map&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;$http_user_agent&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;$blocked_user_agent&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;# Requests are allowed by default&lt;/span&gt;
    &lt;span class=&quot;token directive&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;default&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;token comment&quot;&gt;# `~example` will match any user agent strings&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;# that have `example` anywhere inside them.&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;# Some examples:&lt;/span&gt;
    ~Amazonbot &lt;span class=&quot;token directive&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;1&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    ~openai &lt;span class=&quot;token directive&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;1&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    ~chatgpt &lt;span class=&quot;token directive&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;1&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    ~gptbot &lt;span class=&quot;token directive&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;1&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This list can get as short or as long as you need, and you can change it
(whether to add or remove blocked user agents) anytime you need.&lt;/p&gt;
&lt;h2 id=&quot;block-requests-based-on-blocked-user-agent&quot; tabindex=&quot;-1&quot;&gt;Block requests based on &lt;code&gt;$blocked_user_agent&lt;/code&gt; &lt;a class=&quot;header-anchor&quot; href=&quot;https://aitorres.com/blog/block-user-agents-on-nginx-config/#block-requests-based-on-blocked-user-agent&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Now that you have an easily-accessible map of user agents, it&#39;s time to make
this variable available to &lt;code&gt;nginx&lt;/code&gt; and block unwanted requests.&lt;/p&gt;
&lt;p&gt;On &lt;code&gt;/etc/nginx/nginx.conf&lt;/code&gt;, add the following at the end the &lt;code&gt;http&lt;/code&gt; block:&lt;/p&gt;
&lt;pre class=&quot;language-nginx&quot; tabIndex=&quot;0&quot;&gt;&lt;code class=&quot;language-nginx&quot;&gt;&lt;span class=&quot;token directive&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;http&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;# Skipping over content...&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;# (...)&lt;/span&gt;

    &lt;span class=&quot;token comment&quot;&gt;# Include file with map of blocked user agents&lt;/span&gt;
    &lt;span class=&quot;token directive&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;include&lt;/span&gt; /etc/nginx/blocked_user_agent.rules&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Finally, on the config files for each of your sites (inside
&lt;code&gt;/etc/nginx/sites-enabled/&lt;/code&gt;) add the following inside the &lt;code&gt;server&lt;/code&gt; block, before
you start matching for any &lt;code&gt;location&lt;/code&gt;s:&lt;/p&gt;
&lt;pre class=&quot;language-nginx&quot; tabIndex=&quot;0&quot;&gt;&lt;code class=&quot;language-nginx&quot;&gt;&lt;span class=&quot;token directive&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;server&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token directive&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;server_name&lt;/span&gt; aitorres.com&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;token comment&quot;&gt;# Blocking undesired user agents&lt;/span&gt;
    &lt;span class=&quot;token directive&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; (&lt;span class=&quot;token variable&quot;&gt;$blocked_user_agent&lt;/span&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token comment&quot;&gt;# `444 No Response`, nginx specific HTTP status code.&lt;/span&gt;
        &lt;span class=&quot;token comment&quot;&gt;# You can choose to return other standard HTTP&lt;/span&gt;
        &lt;span class=&quot;token comment&quot;&gt;# status codes, like `404 Not Found` or `403 Forbidden`&lt;/span&gt;
        &lt;span class=&quot;token comment&quot;&gt;# base on your needs&lt;/span&gt;
        &lt;span class=&quot;token directive&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;444&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;token comment&quot;&gt;# Rest of your usual file, unchanged&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;One more thing: reload or restart your &lt;code&gt;nginx&lt;/code&gt; server from your shell.&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot; tabIndex=&quot;0&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# Ensuring config is valid&lt;/span&gt;
nginx &lt;span class=&quot;token parameter variable&quot;&gt;-t&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;# Reloading the server without downtime, you can choose to restart as well&lt;/span&gt;
nginx &lt;span class=&quot;token parameter variable&quot;&gt;-s&lt;/span&gt; reload&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;All done! Your server will start blocking these requests, and you should start
seeing reduced resource consumption. If you have access logs enabled for your
server, then you&#39;ll see the requests from blocked user agents logged with the
HTTP status code you chose to return.&lt;/p&gt;
&lt;p&gt;One final note: if you ever modify the list of blocked user agents, remember to
reload or restart &lt;code&gt;nginx&lt;/code&gt; for the changes to take effect.&lt;/p&gt;
&lt;p&gt;This method is not infalible as it depends on the bot (or the malicious agent
behind it) to consistently use the same user agent, but it&#39;s a start and just
takes a couple minutes to add. Hopefully one day bots will stop misbehaving
completely, but until then... ;-)&lt;/p&gt;
</content>
	</entry>
	
	<entry>
		<title>Handle errors on custom Elementor Form Actions</title>
		<link href="https://aitorres.com/blog/handle-errors-on-custom-elementor-form-action/"/>
		<updated>2024-09-22T00:00:00Z</updated>
		<id>https://aitorres.com/blog/handle-errors-on-custom-elementor-form-action/</id>
		<content type="html">&lt;p&gt;If you&#39;re using the Elementor plug-in for WordPress, you might eventually need
to customize one or several submit actions for Elementor Forms. For example, you
might need to call a specific API on form submission or create custom rows in
your database when the built-in actions (such as sending emails or saving
submissions) fall short.&lt;/p&gt;
&lt;p&gt;The official
&lt;a href=&quot;https://developers.elementor.com/docs/form-actions/&quot;&gt;Elementor Form Actions documentation&lt;/a&gt;
contains an introduction to form actions, including two code samples for
creating new ones. However, none of the examples (or the rest of the
documentation) showcase how to handle errors (e.g. how to inform the front-end
that you can&#39;t process the data any further and the user must review their
answers).&lt;/p&gt;
&lt;p&gt;To achieve this, you can try the following code snippet:&lt;/p&gt;
&lt;pre class=&quot;language-php&quot; tabIndex=&quot;0&quot;&gt;&lt;code class=&quot;language-php&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function-definition function&quot;&gt;run&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;$record&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;$ajax_handler&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;// This is the function that runs your custom action&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;// to handle an Elementor Form submit&lt;/span&gt;

  &lt;span class=&quot;token comment&quot;&gt;// Let&#39;s try to do something that (we know) can fail&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;try&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token function&quot;&gt;action_that_can_fail&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;catch&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Exception&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;$e&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;// Adding the exception message to the AJAX handler&lt;/span&gt;
    &lt;span class=&quot;token variable&quot;&gt;$ajax_handler&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;add_error_message&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;$e&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;getMessage&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;token comment&quot;&gt;// You can also set the error to a specific field&lt;/span&gt;
    &lt;span class=&quot;token variable&quot;&gt;$ajax_handler&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;add_error&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
      &lt;span class=&quot;token string single-quoted-string&quot;&gt;&#39;username&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token variable&quot;&gt;$e&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;getMessage&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token constant boolean&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Note that this code and its behavior are similar to those in the
&lt;a href=&quot;https://developers.elementor.com/docs/form-fields/field-validation/&quot;&gt;Elementor Form Fields documentation&lt;/a&gt;
which I discovered after starting to write this blog post. However, since it&#39;s
not mentioned on the Form Actions documentation, I hope this post serves as a
bridge between the two for anyone wondering how to gracefully handle errors on
form submission for their custom actions.&lt;/p&gt;
</content>
	</entry>
	
	<entry>
		<title>A fresh start</title>
		<link href="https://aitorres.com/blog/a-fresh-start/"/>
		<updated>2024-09-08T00:00:00Z</updated>
		<id>https://aitorres.com/blog/a-fresh-start/</id>
		<content type="html">&lt;p&gt;Hello there! If you&#39;re reading this, welcome! And thanks! (And also: why!?)&lt;/p&gt;
&lt;p&gt;This is (hopefully) my definite attempt to run a blog as a space to drop random
thoughts, ideas and ramblings in public, and to improve my online presence. Or,
at least, try to.&lt;/p&gt;
&lt;p&gt;This first post doesn&#39;t have much content by itself, but is a testament to the
day I finally got around to writing some minor code and (re-)starting my blog...
after adding a task to my to-do list almost four years ago.&lt;/p&gt;
&lt;p&gt;&lt;picture&gt;&lt;source type=&quot;image/webp&quot; srcset=&quot;https://aitorres.com/img/a-fresh-task.webp 310w&quot; sizes=&quot;100vw&quot;&gt;&lt;img src=&quot;https://aitorres.com/img/a-fresh-task.png&quot; alt=&quot;A screenshot from the Todoist application, showing a task created on December 2020.&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; width=&quot;310&quot; height=&quot;94&quot;&gt;&lt;/picture&gt;&lt;/p&gt;
&lt;figcaption&gt;I guess I&#39;ve been planning on doing this for quite some time...&lt;/figcaption&gt;
&lt;p&gt;I expect to expand on both the blog posts and the personal sections in the next
few days (but if I had to set a due date, I&#39;d pick December 2028, just to be
consistent).&lt;/p&gt;
&lt;p&gt;So, again, thanks for taking the time to explore my little outpost in the
cyberspace! Hope to catch you again around here soon!&lt;/p&gt;
</content>
	</entry>
</feed>
