Paragraph is a blazing-fast, privacy-first, no-frills-attached blogging & newsletter platform. Sign up and start writing posts just like this one.
Productivity & automation tools can be powerful. I was delighted to come across a recent thread on Hacker News discussing a supercharged automation tool: Huginn. This open-source software performs automated tasks by using 'agents' to watch for 'events', and triggering 'actions' based on these events.
For example, if there's a sudden spike in discussion on Twitter with the terms "San Francisco Earthquake", Huginn can send a text to my phone. Or, if a time-sensitive flight deal is posted on one of the many deal-finding websites out there, Huginn can send me an email with the price and a link to Google Flights.
Compared to other popular automation tools (IFTTT, Zapier), Huginn has the following benefits:
Paragraph is a blazing-fast, privacy-first, no-frills-attached blogging & newsletter platform. Sign up and start writing posts just like this one.
Productivity & automation tools can be powerful. I was delighted to come across a recent thread on Hacker News discussing a supercharged automation tool: Huginn. This open-source software performs automated tasks by using 'agents' to watch for 'events', and triggering 'actions' based on these events.
For example, if there's a sudden spike in discussion on Twitter with the terms "San Francisco Earthquake", Huginn can send a text to my phone. Or, if a time-sensitive flight deal is posted on one of the many deal-finding websites out there, Huginn can send me an email with the price and a link to Google Flights.
Compared to other popular automation tools (IFTTT, Zapier), Huginn has the following benefits:
Self-hosted & completely private
Powerful data processing: write your own JS or use shell scripts
Liquid templating
I wanted to go beyond just automation and introduce some organization - I wanted all notifications to be cataloged & delivered in a centralized way. A personal Slack workspace seemed like the perfect solution for this - I can have a #flights channel for flight deals, or a #trending channel for the, er, pending San Francisco emergencies.
I also wanted all of this to be free. Huginn has pretty lax runtime resource requirements (even able to run on a Raspberry Pi, with some tweaking), so a free GCP micro tier instance was perfect for this.
Automation Goals
Let's formalize what I specifically wanted to accomplish with Huginn. Note that this is a small subset of the things possible with Huginn - check out the project's Github for more inspiration.
Twitter notifications: whenever keywords of interest are tweeted (such as my projects or blog), I want to get notified immediately. Whenever a spike occurs for other keywords ("San Francisco Emergency"), notify me.
Hacker news notifications: whenever an article hits the frontpage discussing something I'm interested in, notify me.
Flight deals: if a flight deal is posted online to one of the many websites I follow (Secret Flying, ThePointsGuy, FlyerTalk), and the flight originates from a nearby airport, notify me.
Product deals: if a product I'm interested in is posted on Slickdeals, notify me.
Amazon price drops: if a product I'm interested in drops below some predefined price threshold, notify me.
I want all notifications to be sent to me via a personal Slack workspace, on different channels.
Deploying Huginn
Prerequisites
The easiest way to install Huginn is via Docker. Luckily, Google Compute Engine supports deploying Docker containers natively on a lean container-optimized OS.
There are a few key things we need to do in order to have a successful Huginn deploy on the f1-micro (free tier) instances.
Enable and create a swap file.
f1-micro instances have 614MB of memory. This is not enough to run Huginn out of the box - doing so will cause Docker to encounter Error Code 137(out of memory) errors. To solve this, we need to create a swap file in the VM. Note that a swapfile will decrease the performance of Huginn - if you're interested in better performance for a price, consider deploying on a better VM.
Disk-based swap is disabled by default in container-optimized OS. To enable and set the swap file every time the VM is booted, we can use a custom startup script (shown below).
Mount the Huginn MySQL database to a directory on the host.
By default, Huginn creates a MySQL database inside the container. This is problematic, as the container now relies on state, and your database will get deleted every Huginn upgrade. We can use a volume mount to mount the database in the container to a directory in the host. Alternatively, you can mount a persistent disk and write the database to it.
Deployment on GCP using Docker
Head over to GCP, create a new project, and create a new instance.
On the instance creation page, use the following settings:
f1-micro machine type
Check 'Deploy a container image to this VM instance'
Container image URL is docker.io/huginn/huginn
Add a Directory volume mount. The mount and host paths should be /var/lib/mysql
We also need to add in a statup script. This script lets us 1) enable and turn on a swap file, and 2) change permissions of the volume mount on the host. The latter is required or MySQL won't be able to start.
At a high level, Huginn relies on two key things: agents and events. Agents are things that monitor for you and create events (possibly if some criteria is met). An example agent is an RssAgent, which monitors an RSS feed for new articles. Events created by the RssAgent can be passed to a TriggerAgent, which uses some regex filter to only listen to keywords of interest; and finally it emits a formatted message, perhaps to a SlackAgent, that finally sends a message to a Slack channel.
You can imagine how this works in practice. For the flight deals usecase, for example: we can create an RssAgent for the Secret Flying RSS feed. The TriggerAgent can listen to these events, filter for "San Francisco Airport", and the SlackAgent can message my #flights channel when this happens
Multiple agents for a single usecase can be grouped into a Scenario - in the above example, a Flight Deal Scenario would make sense.
Let's walk through this example. If you want to get started immediately, you can download my agents and import them into Huginn directly.
Creating your First Agent
1) Monitoring the RSS feed
On Huginn, create a new RSS Agent. Configure the following params:
Name your agent something descriptive. I used "Secret Flying RSS Agent".
Schedule your agent for however frequently you'd like it to check for updates. I used 30 mins.
Keep events for some period of time. I used 7 days.
Expected update period is the period at which Huginn should expect the agent to be updated - if it doesn't happen, the agent is considered not working.
Save your agent, and give it a manual run - you should see events populate from the underlying feed.
2) Filtering for nearby airports
Now, create a TriggerAgent. We want to filter newly posted articles for only nearby airports - in my case, San Francisco or San Jose airport.
Fill it out similar to the first agent. But, this time select your RSS Agent as this agent's source. Events from the RSS Agent will be fed into this.
Save your agent, and eventually you should begin receiving flight deals!
Wrapping it up
This example describes a single usecase for what's possible with Huginn. If you're interested in the other usecases I described above, you can download and import them into your own installation.
This post just scratches the surface of what's possible. With Huginn, let your Agents monitor on your behalf and free up your time for more important things.
NextJS is powerful: it brings together the best of server-side to the best of client-side. You can build powerful applications written in React, and produce a static website which has all the speed of a regular ol’ CDN-backed website. Despite the benefits, you may sometimes hit some challenges working with both server-side rendering (SSR) and client-side hydration. One of the more common errors is when the server-side and client-side DOM doesn’t match. NextJS might complain:
Expected server HTMLtocontaina matching <div> in <div>
Warning: Text content did not match. Server: "Log in"Client: "Continue to dashboard"
Additionally, the HTML (in particular, the DOM elements) may not render correctly when viewing the webpage.
Why is this happening?
NextJS uses both server-side and client-side rendering together. The server serves static HTML whenever possible, and the client hydrates it to give full interactivity. If you manipulate the DOM at any time between when the server-side HTML is served and the client-side is rendered, there will be a mismatch and React will not be able to hydrate the DOM successfully. Thus, the first render of any page must match the initial render of the server. This is explained more in-depth in this Github issue. I encountered this issue when relying on cookies. I was doing something like this:
const user = Cookies.get('jwt')
if (user?.id) {
return<span>User logged in!</span>
}
return<span>User not logged in!</span>
Cookies are only accessible in the browser, so when the server provides the static HTML the user would not be logged in, but when the client is rendered, the user would be logged in! Hence, there is a mismatch and NextJS will complain.
How do I fix this?
Put all browser-only code inside useEffect. This ensures that the client is rendered before the DOM is updated. This means the client-side and server-side DOM will match.
JS Tip: useEffect is a React hook that lets you issue side effects - such as updating the DOM - after the page or component is mounted. The above example can be refactored:
const [isUserLoggedIn, setIsUserLoggedIn] = useState(false)
const user = Cookies.get('jwt')
useEffect(() => {
setIsUserLoggedIn(!!user?.id)
}, [user])
if (isUserLoggedIn) {
return<span>User logged in!</span>
}
return<span>User not logged in!</span>
For some reason, on my site, my cursor was not working correctly. When hovering over links or buttons or elements that set cursor: pointer, my cursor did not turn into a pointer hand; instead, it stayed as a regular ol’ cursor.
This was odd. I didn’t change any styling that could affect this. I took to Google, and the first link gave me the right answer:
I saw that I was indeed running Photoshop at the time. I closed it, and my cursor began acting normally.
How strange!
If anyone can shed light as to why Photshop and other programs do this, please let me know!o
Self-hosted & completely private
Powerful data processing: write your own JS or use shell scripts
Liquid templating
I wanted to go beyond just automation and introduce some organization - I wanted all notifications to be cataloged & delivered in a centralized way. A personal Slack workspace seemed like the perfect solution for this - I can have a #flights channel for flight deals, or a #trending channel for the, er, pending San Francisco emergencies.
I also wanted all of this to be free. Huginn has pretty lax runtime resource requirements (even able to run on a Raspberry Pi, with some tweaking), so a free GCP micro tier instance was perfect for this.
Automation Goals
Let's formalize what I specifically wanted to accomplish with Huginn. Note that this is a small subset of the things possible with Huginn - check out the project's Github for more inspiration.
Twitter notifications: whenever keywords of interest are tweeted (such as my projects or blog), I want to get notified immediately. Whenever a spike occurs for other keywords ("San Francisco Emergency"), notify me.
Hacker news notifications: whenever an article hits the frontpage discussing something I'm interested in, notify me.
Flight deals: if a flight deal is posted online to one of the many websites I follow (Secret Flying, ThePointsGuy, FlyerTalk), and the flight originates from a nearby airport, notify me.
Product deals: if a product I'm interested in is posted on Slickdeals, notify me.
Amazon price drops: if a product I'm interested in drops below some predefined price threshold, notify me.
I want all notifications to be sent to me via a personal Slack workspace, on different channels.
Deploying Huginn
Prerequisites
The easiest way to install Huginn is via Docker. Luckily, Google Compute Engine supports deploying Docker containers natively on a lean container-optimized OS.
There are a few key things we need to do in order to have a successful Huginn deploy on the f1-micro (free tier) instances.
Enable and create a swap file.
f1-micro instances have 614MB of memory. This is not enough to run Huginn out of the box - doing so will cause Docker to encounter Error Code 137(out of memory) errors. To solve this, we need to create a swap file in the VM. Note that a swapfile will decrease the performance of Huginn - if you're interested in better performance for a price, consider deploying on a better VM.
Disk-based swap is disabled by default in container-optimized OS. To enable and set the swap file every time the VM is booted, we can use a custom startup script (shown below).
Mount the Huginn MySQL database to a directory on the host.
By default, Huginn creates a MySQL database inside the container. This is problematic, as the container now relies on state, and your database will get deleted every Huginn upgrade. We can use a volume mount to mount the database in the container to a directory in the host. Alternatively, you can mount a persistent disk and write the database to it.
Deployment on GCP using Docker
Head over to GCP, create a new project, and create a new instance.
On the instance creation page, use the following settings:
f1-micro machine type
Check 'Deploy a container image to this VM instance'
Container image URL is docker.io/huginn/huginn
Add a Directory volume mount. The mount and host paths should be /var/lib/mysql
We also need to add in a statup script. This script lets us 1) enable and turn on a swap file, and 2) change permissions of the volume mount on the host. The latter is required or MySQL won't be able to start.
At a high level, Huginn relies on two key things: agents and events. Agents are things that monitor for you and create events (possibly if some criteria is met). An example agent is an RssAgent, which monitors an RSS feed for new articles. Events created by the RssAgent can be passed to a TriggerAgent, which uses some regex filter to only listen to keywords of interest; and finally it emits a formatted message, perhaps to a SlackAgent, that finally sends a message to a Slack channel.
You can imagine how this works in practice. For the flight deals usecase, for example: we can create an RssAgent for the Secret Flying RSS feed. The TriggerAgent can listen to these events, filter for "San Francisco Airport", and the SlackAgent can message my #flights channel when this happens
Multiple agents for a single usecase can be grouped into a Scenario - in the above example, a Flight Deal Scenario would make sense.
Let's walk through this example. If you want to get started immediately, you can download my agents and import them into Huginn directly.
Creating your First Agent
1) Monitoring the RSS feed
On Huginn, create a new RSS Agent. Configure the following params:
Name your agent something descriptive. I used "Secret Flying RSS Agent".
Schedule your agent for however frequently you'd like it to check for updates. I used 30 mins.
Keep events for some period of time. I used 7 days.
Expected update period is the period at which Huginn should expect the agent to be updated - if it doesn't happen, the agent is considered not working.
Save your agent, and give it a manual run - you should see events populate from the underlying feed.
2) Filtering for nearby airports
Now, create a TriggerAgent. We want to filter newly posted articles for only nearby airports - in my case, San Francisco or San Jose airport.
Fill it out similar to the first agent. But, this time select your RSS Agent as this agent's source. Events from the RSS Agent will be fed into this.
Save your agent, and eventually you should begin receiving flight deals!
Wrapping it up
This example describes a single usecase for what's possible with Huginn. If you're interested in the other usecases I described above, you can download and import them into your own installation.
This post just scratches the surface of what's possible. With Huginn, let your Agents monitor on your behalf and free up your time for more important things.
NextJS is powerful: it brings together the best of server-side to the best of client-side. You can build powerful applications written in React, and produce a static website which has all the speed of a regular ol’ CDN-backed website. Despite the benefits, you may sometimes hit some challenges working with both server-side rendering (SSR) and client-side hydration. One of the more common errors is when the server-side and client-side DOM doesn’t match. NextJS might complain:
Expected server HTMLtocontaina matching <div> in <div>
Warning: Text content did not match. Server: "Log in"Client: "Continue to dashboard"
Additionally, the HTML (in particular, the DOM elements) may not render correctly when viewing the webpage.
Why is this happening?
NextJS uses both server-side and client-side rendering together. The server serves static HTML whenever possible, and the client hydrates it to give full interactivity. If you manipulate the DOM at any time between when the server-side HTML is served and the client-side is rendered, there will be a mismatch and React will not be able to hydrate the DOM successfully. Thus, the first render of any page must match the initial render of the server. This is explained more in-depth in this Github issue. I encountered this issue when relying on cookies. I was doing something like this:
const user = Cookies.get('jwt')
if (user?.id) {
return<span>User logged in!</span>
}
return<span>User not logged in!</span>
Cookies are only accessible in the browser, so when the server provides the static HTML the user would not be logged in, but when the client is rendered, the user would be logged in! Hence, there is a mismatch and NextJS will complain.
How do I fix this?
Put all browser-only code inside useEffect. This ensures that the client is rendered before the DOM is updated. This means the client-side and server-side DOM will match.
JS Tip: useEffect is a React hook that lets you issue side effects - such as updating the DOM - after the page or component is mounted. The above example can be refactored:
const [isUserLoggedIn, setIsUserLoggedIn] = useState(false)
const user = Cookies.get('jwt')
useEffect(() => {
setIsUserLoggedIn(!!user?.id)
}, [user])
if (isUserLoggedIn) {
return<span>User logged in!</span>
}
return<span>User not logged in!</span>
For some reason, on my site, my cursor was not working correctly. When hovering over links or buttons or elements that set cursor: pointer, my cursor did not turn into a pointer hand; instead, it stayed as a regular ol’ cursor.
This was odd. I didn’t change any styling that could affect this. I took to Google, and the first link gave me the right answer:
I saw that I was indeed running Photoshop at the time. I closed it, and my cursor began acting normally.
How strange!
If anyone can shed light as to why Photshop and other programs do this, please let me know!o