M config.toml => config.toml +0 -1
@@ 2,7 2,6 @@ base_url = "https://myrrc.dev"
title = "Mikhail Kot"
author = "Mikhail Kot"
description = "Mikhail Kot's blog"
-compile_sass = true
minify_html = true
generate_feed = true
feed_filename = "rss.xml"
A content/2024-08-20-nfmd.md => content/2024-08-20-nfmd.md +118 -0
@@ 0,0 1,118 @@
++++
+title = "NFMD: A news feed mailer daemon"
++++
+
+NFMD is a program which grabs news feeds from a specified file and sends daily updates via e-mail.
+<!-- more -->
+It parses RSS/Atom feeds natively, and for other sources you can pass a service that converts
+data to RSS i.e. [RSS hub](https://docs.rsshub.app/) instances.
+
+Links:
+- [nfmd-0.3.deb](/nfmd-0.3.deb) for Debian 12 Bookworm
+- [sources](https://git.sr.ht/~myrrc/nfmd)
+
+## Usage
+
+- Get your favourite feeds in a config text file:
+```
+18:05 email@foogle.com
+[header]
+<style>#video { display: none; }</style>
+[schemes]
+https://t.me https://rsshub.app/telegram/channel
+[urls]
+# This page provides RSS natively
+https://myrrc.dev/rss.xml # This is an inline comment
+# This page doesn't so we'll query rsshub.app
+https://t.me/red_spades
+```
+
+- Install deb package or compile from source and copy binary to a server
+- Run `nfmd -c *config* set`
+
+## Why
+I follow around 100 Telegram channels and also other sources like YouTube, Reddit, as well as many
+small websites. At some point I decided I want everything in a single place. Although most
+people I know use Telegram for that purpose, messenger style
+doesn't seem reasonable due to continuous distraction. Even if you mute notifications and archive
+chats, you still have items to click through. This is solved in Telegram mobile with
+swipe up gesture, taking you to next channel or dialog, however, on desktop there is no such
+functionality. Another idea is to separate messaging and news reading as for me these are
+two different activities.
+
+Therefore another solution was searched. The easy way was to convert everything to RSS. There
+are many clever apps like [ratt](https://git.sr.ht/~ghost08/ratt) but the need to
+write manual rules was a bit frustrating, so I discovered [RSS hub](https://docs.rsshub.app/).
+A simple and elegant one - pass a link, get RSS. If you don't want to rely on public instances,
+host your own.
+
+Next bike-shedding step was to find an RSS reader. Most of my computer time except for the browser
+is spent in the terminal. Terminal readers like [photon](https://git.sr.ht/~ghost08/photon/) may be
+awesome, and I tried using them, but there still are downsides:
+- You need to have the reader up and running in order to fetch new data.
+- Rss readers don't usually have a good way to aggregate data per day.
+- In rare cases when you need to read from mobile, you need another app.
+
+The final decision was to use e-mail. It's simple, it handles unread items, you can perform
+fetching and filtering on the server in case your computer is turned off.
+
+## How
+The initial plan included writing a daemon which would wake up at specified time, fetch
+sources from a TOML file in parallel, filter them to get last ones only, convert to
+HTML and send an email to me.
+I chose Rust, picked up some libraries and made a prototype. It was... big, around 20MB
+total. Playing with musl builds, removing libunwind, no luck.
+Such a simple program definitely shouldn't be that big.
+
+So I reconsidered the functionality:
+- Removed all authentication. I run a Postfix/Dovecot e-mail server so program
+ can just send to localhost.
+- Removed complex wakeup rules. I realised I really wanted news only one time a day
+- Removed tokio and parallelism. One consideration was the complexity - even if serial
+ execution is way slower[^1], I need the program to be executed one time a day only.
+ Second - multiple concurrent requests in a short period can lead to being
+ blocked by source servers.
+ Third - my server has only one core, so many threads wouldn't bring much benefit.
+- Removed TOML in favor of plain text. With a list of URLs, any markup is excessive.
+- Replaced manual demonization with cron. In most systems cron or anacron is available,
+ and you can use it for 1) reliable wakeups, and 2) restarts if program crashed.
+ Also, a long-running program is something that may cause pain if there's a memory leak
+ or some other bug.
+
+After refactoring, the following dependencies were left:
+- data fetching via [curl](https://crates.io/crates/curl) bindings.
+- email sending via [lettre](https://crates.io/crates/lettre)
+- data parsing via [rss](https://crates.io/crates/rss) and
+ [atom](https://crates.io/crates/atom_syndication)
+
+Then I read curl can send SMTP requests, however, the Rust crate didn't have this,
+so I decided to switch to C[^2].
+
+## Bells and whistles
+
+The next prototype was way faster and smaller, around 14kb, but still didn't feel ideally
+comfortable. I added missing functionality:
+- Comments after URLs for tagging various sources
+- Prefix replacement so that you could add `https://t.me/tokacomics` and not
+ `https://rsshub.rss.tips/telegram/channel/tokacomics`.
+- E-mail styling via inline CSS: customizable, my styles specifically were width
+ formatting mostly:
+```
+a { text-decoration: none; }
+video { display:none; }
+body,img { width: 600px; }
+body {
+ padding-left:25%;
+ padding-right:25%;
+}
+```
+
+## Packaging
+
+Debian packages as they're 1) easy to build via pbuilder 2) easy to install on target server 3)
+(relatively) easy to write.
+
+[^1]: Not necessarily true as such programs are usually io-bound
+
+[^2]: Why not C++? Most easily discovered libraries for my task are C ones, and the code would be
+ a mix of two languages which is bad for readability.
D sass/style.sass => sass/style.sass +0 -315
@@ 1,315 0,0 @@
-@charset "utf-8"
-$size: 16px !default
-$weight: 400 !default
-$small-font-size: $size * 0.875 !default
-$base-line-height: 1.5 !default
-$spacing: 30px !default
-$table-text-align: left !default
-$content: 800px !default
-$lm-brand-color: #828282 !default
-$lm-brand-color-light: lighten($lm-brand-color, 40%) !default
-$lm-brand-color-dark: darken($lm-brand-color, 25%) !default
-$lm-site-title-color: $lm-brand-color-dark !default
-$lm-text-color: #111111 !default
-$lm-background-color: #fdfdfd !default
-$lm-code-background-color: #eeeeff !default
-$lm-link: #2a7ae2 !default
-$lm-link-visited: darken($lm-link, 15%) !default
-$lm-link-hover: $lm-text-color !default
-$lm-border-color-01: $lm-brand-color-light !default
-$lm-border-color-02: lighten($lm-brand-color, 35%) !default
-$lm-border-color-03: $lm-brand-color-dark !default
-$lm-table-text-color: lighten($lm-text-color, 18%) !default
-$lm-table-zebra-color: lighten($lm-brand-color, 46%) !default
-$lm-table-header-bg-color: lighten($lm-brand-color, 43%) !default
-$lm-table-header-border: lighten($lm-brand-color, 37%) !default
-$lm-table-border-color: $lm-border-color-01 !default
-$dm-brand-color: #999999 !default
-$dm-brand-color-light: lighten($dm-brand-color, 5%) !default
-$dm-brand-color-dark: darken($dm-brand-color, 35%) !default
-$dm-site-title-color: $dm-brand-color-light !default
-$dm-text-color: #bbbbbb !default
-$dm-background-color: #181818 !default
-$dm-code-background-color: #212121 !default
-$dm-link: #79b8ff !default
-$dm-link-visited: $dm-link !default
-$dm-link-hover: $dm-text-color !default
-$dm-border-color-01: $dm-brand-color-dark !default
-$dm-border-color-02: $dm-brand-color-light !default
-$dm-border-color-03: $dm-brand-color !default
-$dm-table-text-color: $dm-text-color !default
-$dm-table-zebra-color: lighten($dm-background-color, 4%) !default
-$dm-table-header-bg-color: lighten($dm-background-color, 10%) !default
-$dm-table-header-border: lighten($dm-background-color, 21%) !default
-$dm-table-border-color: $dm-border-color-01 !default
-
-:root
- --v: #{$lm-brand-color}
- --light: #{$lm-brand-color-light}
- --dark: #{$lm-brand-color-dark}
- --title: #{$lm-site-title-color}
- --text: #{$lm-text-color}
- --bg: #{$lm-background-color}
- --code: #{$lm-code-background-color}
- --link: #{$lm-link}
- --linkv: #{$lm-link-visited}
- --linkh: #{$lm-link-hover}
- --b1: #{$lm-border-color-01}
- --b2: #{$lm-border-color-02}
- --b3: #{$lm-border-color-03}
- --tbl: #{$lm-table-text-color}
- --tblz: #{$lm-table-zebra-color}
- --tbl-hdr-bg: #{$lm-table-header-bg-color}
- --tbl-hdr-border: #{$lm-table-header-border}
- --tbl-border: #{$lm-table-border-color}
-
-@media (prefers-color-scheme: dark)
- :root
- --v: #{$dm-brand-color}
- --light: #{$dm-brand-color-light}
- --dark: #{$dm-brand-color-dark}
- --title: #{$dm-site-title-color}
- --text: #{$dm-text-color}
- --bg: #{$dm-background-color}
- --code: #{$dm-code-background-color}
- --link: #{$dm-link}
- --linkv: #{$dm-link-visited}
- --linkh: #{$dm-link-hover}
- --b1: #{$dm-border-color-01}
- --b2: #{$dm-border-color-02}
- --b3: #{$dm-border-color-03}
- --tbl: #{$dm-table-text-color}
- --tblz: #{$dm-table-zebra-color}
- --tbl-hdr-bg: #{$dm-table-header-bg-color}
- --tbl-hdr-border: #{$dm-table-header-border}
- --tbl-border: #{$dm-table-border-color}
-
-$brand-color: var(--v)
-$brand-color-light: var(--light)
-$brand-color-dark: var(--dark)
-$site-title-color: var(--title)
-$text-color: var(--text)
-$background-color: var(--bg)
-$code-background-color: var(--code)
-$link: var(--link)
-$link-visited: var(--linkv)
-$link-hover: var(--linkh)
-$border-color-01: var(--b1)
-$border-color-02: var(--b2)
-$border-color-03: var(--b3)
-$table-text-color: var(--tbl)
-$table-zebra-color: var(--tblz)
-$table-header-bg-color: var(--tbl-hdr-bg)
-$table-header-border: var(--tbl-hdr-border)
-$table-border-color: var(--tbl-border)
-
-html
- font-size: $size
-
-body, h1, h2, h3, h4, h5, h6, p, blockquote, pre, hr, dl, dd, ol, ul, figure
- margin: 0
- padding: 0
-
-body
- font: $weight #{$size}/#{$base-line-height} sans-serif
- color: $text-color
- background-color: $background-color
- -webkit-text-size-adjust: 100%
- font-feature-settings: "kern" 1
- font-kerning: normal
- display: flex
- min-height: 100vh
- flex-direction: column
- overflow-wrap: break-word
-
-h1, h2, h3, h4, h5, h6, p, blockquote, pre, ul, ol, dl, figure, details, %vertical-rhythm
- margin-bottom: $spacing / 2
-
-hr
- margin-top: $spacing
- margin-bottom: $spacing
-
-main
- display: block
-
-img
- max-width: 100%
- vertical-align: middle
-
-figure > img
- display: block
-
-figcaption
- font-size: $small-font-size
-
-ul, ol
- margin-left: $spacing
-
-li > ul, li > ol
- margin-bottom: 0
-
-h1, h2, h3, h4, h5, h6
- font-weight: $weight
-
-a
- color: $link
- text-decoration: none
-
- &:visited
- color: $link-visited
- &:hover
- color: $link-hover
- text-decoration: underline
-
-blockquote
- border-left: 4px solid $border-color-03
- padding-left: $spacing / 2
-
- > :last-child
- margin-bottom: 0
-
-pre, code
- font-family: monospace
- font-size: 0.9375em
- border: 1px solid $border-color-01
- border-radius: 3px
- background-color: $code-background-color
-
-code
- padding: 1px 5px
-
-pre
- padding: 8px 12px
- overflow-x: auto
-
- > code
- border: 0
- padding-right: 0
- padding-left: 0
-
-.highlight
- border-radius: 3px
- background: $code-background-color
- @extend %vertical-rhythm
-
-.wrapper
- max-width: $content - $spacing
- margin-right: auto
- margin-left: auto
- padding-right: $spacing / 2
- padding-left: $spacing / 2
- @extend %clearfix
-
- @media screen and (min-width: $content)
- max-width: $content - $spacing * 2
- padding-right: $spacing
- padding-left: $spacing
-
-%clearfix:after
- content: ""
- display: table
- clear: both
-
-table
- margin-bottom: $spacing
- width: 100%
- text-align: $table-text-align
- color: $table-text-color
- border-collapse: collapse
- border: 1px solid $table-border-color
-
- tr:nth-child(even)
- background-color: $table-zebra-color
- th, td
- padding: ($spacing / 3) ($spacing / 2)
- th
- background-color: $table-header-bg-color
- border: 1px solid $table-header-border
- td
- border: 1px solid $table-border-color
-
- @media screen and (max-width: $content)
- display: block
- overflow-x: auto
- -webkit-overflow-scrolling: touch
- -ms-overflow-style: -ms-autohiding-scrollbar
-
-.site-header
- min-height: $spacing * 1.865
- line-height: $base-line-height * $size * 2.25
- position: relative
-
-.site-title
- font-size: 1.625rem
- font-weight: 300
- letter-spacing: -1px
- margin-bottom: 0
- float: left
-
- &, &:visited
- color: $site-title-color
-
-.popcat
- margin-left: $spacing / 5
- margin-top: -$spacing / 5
- border-radius: 15%
-
-.page-content
- padding: 0
- flex: 1 0 auto
-
-.page-heading
- font-size: 2rem
-
-.post-link
- display: block
- font-size: 1.5rem
-
-.post-header
- margin-bottom: $spacing / 2
-
-.post-header h1, .post-content h1
- font-size: 2.625rem
- letter-spacing: -1px
- line-height: 1.15
-
- @media screen and (min-width: $content)
- font-size: 2.625rem
-
-.post-content
- margin-bottom: $spacing
-
- h1, h2, h3, h4, h5, h6
- margin-top: $spacing
- h2
- font-size: 1.75rem
-
- @media screen and (min-width: $content)
- font-size: 2rem
- h3
- font-size: 1.375rem
-
- @media screen and (min-width: $content)
- font-size: 1.625rem
- h4
- font-size: 1.25rem
- h5
- font-size: 1.125rem
- h6
- font-size: 1.0625rem
-
-.quote
- text-align: right
-
-footer, .section-cut
- text-align: center
-
-footer
- padding-bottom: 10px
-
-.lang
- float: right
-.row
- display: flex
-.col
- flex: 50%
A static/nfmd-0.3.deb => static/nfmd-0.3.deb +0 -0
A static/style.css => static/style.css +71 -0
@@ 0,0 1,71 @@
+:root{
+--title: #424242; --text: #111; --bg: #fdfdfd; --code: #eef;
+--link: #2a7ae2; --linkv: #1756a9; --linkh: #111;
+--b1: #e8e8e8; --b2: #424242;
+--tbl: #3f3f3f; --tblz: #f7f7f7; --th-bg: #f0f0f0;
+--th-border: #e0e0e0; --tbl-border: #e8e8e8
+}
+@media (prefers-color-scheme: dark){:root{
+--title: #a6a6a6; --text: #bbb; --bg: #181818; --code: #212121;
+--link: #79b8ff; --linkv: #79b8ff; --linkh: #bbb;
+--b1: #404040; --b2: #999;
+--tbl: #bbb; --tblz: #222; --th-bg: #323232;
+--th-border: #4e4e4e; --tbl-border: #404040
+}}
+html{font-size:16px}
+body,h1,h2,h3,h4,p,blockquote,pre,hr,dl,dd,ol,ul,figure{margin:0;padding:0}
+body{
+ color:var(--text); background-color:var(--bg);
+ font:400 16px/1.5 sans-serif;font-feature-settings:"kern" 1;font-kerning:normal;
+ display:flex;min-height:100vh;flex-direction:column;overflow-wrap:break-word
+}
+h1,h2,h3,h4,p,blockquote,pre,ul,ol,dl,figure,details{margin-bottom:15px}
+hr{margin-top:30px;margin-bottom:30px}
+main{display:block}
+img{max-width:100%;vertical-align:middle}
+figure>img{display:block}
+figcaption{font-size:14px}
+ul,ol{margin-left:30px}
+li>ul,li>ol{margin-bottom:0}
+h1,h2,h3,h4{font-weight:400}
+a{color:var(--link);text-decoration:none}
+a:visited{color:var(--linkv)}
+a:hover{color:var(--linkh);text-decoration:underline}
+blockquote{border-left:4px solid var(--b2);padding-left:15px}
+blockquote>:last-child{margin-bottom:0}
+pre,code{font-family:monospace;font-size:.9375em;border:1px solid var(--b1);border-radius:3px;background-color:var(--code)}
+code{padding:1px 5px}
+pre{padding:8px 12px;overflow-x:auto}
+pre>code{border:0;padding-right:0;padding-left:0}
+.wrapper{max-width:740px;margin-right:auto;margin-left:auto;padding-right:30px;padding-left:30px}
+.wrapper:after{content:"";display:table;clear:both}
+table{margin-bottom:30px;width:100%;text-align:left;color:var(--tbl);border-collapse:collapse;border:1px solid var(--tbl-border)}
+table tr:nth-child(even){background-color:var(--tblz)}
+table th,table td{padding:10px 15px}
+table th{background-color:var(--th-bg);border:1px solid var(--th-border)}
+table td{border:1px solid var(--tbl-border)}
+@media screen and (max-width: 800px){table{display:block;overflow-x:auto}}
+.site-header{min-height:55.95px;line-height:54px;position:relative}
+.site-title{font-size:1.625rem;font-weight:300;letter-spacing:-1px;margin-bottom:0;float:left}
+.site-title,.site-title:visited{color:var(--title)}
+.popcat{margin-left:6px;margin-top:-6px;border-radius:15%}
+.page-content{padding:0;flex:1 0 auto}
+.page-heading{font-size:2rem}
+.post-link{display:block;font-size:1.5rem}
+.post-header{margin-bottom:15px}
+.post-header h1,.post h1{font-size:2.625rem;letter-spacing:-1px;line-height:1.15}
+.post{margin-bottom:30px}
+.post h1,.post h2,.post h3,.post h4{margin-top:30px}
+.post h2{font-size:1.75rem}
+.post h3{font-size:1.375rem}
+.post h4{font-size:1.25rem}
+@media screen and (min-width: 800px){
+ .post h2{font-size:2rem}
+ .post h3{font-size:1.625rem}
+}
+.quote{text-align:right}
+footer,.section-cut{text-align:center}
+footer{padding-bottom:10px}
+.lang{float:right}
+.row{display:flex}
+.col{flex:50%}
M templates/base.html => templates/base.html +4 -4
@@ 1,13 1,13 @@
<!DOCTYPE html>
-<html lang="en">
+<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content="{{ config.description }}">
<meta name="referrer" content="no-referrer-when-downgrade">
- <link rel="stylesheet" href="{{ get_url(path="style.css", trailing_slash=false) | safe }}">
- <link rel="icon" href="{{ get_url(path="favicon.ico") | safe }}">
+ <link rel="stylesheet" href="/style.css">
+ <link rel="icon" href="/favicon.ico">
<link rel="alternate" type="application/rss+xml" title="{{ config.title }}" href="{{ get_url(path=config.feed_filename) | safe }}">
{% block head %} {% endblock %}
</head>
@@ 15,7 15,7 @@
<header class="site-header">
<div class="wrapper">
<a class="site-title" href="{{ config.base_url }}"> {{ config.title }} </a>
- <img class="popcat" src="{{ get_url(path="popcat.gif") }}" alt="popcat"/>
+ <img class="popcat" src="/popcat.gif" alt="popcat"/>
{% block lang %} {% endblock %}
</div>
</header>
M templates/page.html => templates/page.html +1 -3
@@ 31,7 31,5 @@
</ol>
{% endif %}
</header>
-<main>
- <div class="post-content"> {{ page.content | safe }} </div>
-</main>
+<main class="post"> {{ page.content | safe }} </main>
{% endblock %}