+7
-1
README.md
+7
-1
README.md
···
1
# Herald ๐
2
3
-

4
5
This was inspired by the sunsetting of [pico.sh/feeds](https://blog.pico.sh/ann-033-moving-rss-to-email-pico-plus) being available outside of `pico+`. It is a totally understandable move from them as their email costs were skyrocketing and they needed to pay for it somehow. This was created to allow me to still get my rss feeds delivered to me each day by email which I have grown quite accustomed to. The config is completely compatible with the `pico.sh` format as of `2026-01-09` and should stay fairly stable. It is also configured over ssh with the slight addition that you can view your feeds on a website as well as I found myself wanting to hot load my feeds into my website :)
6
···
136
- `HERALD_SMTP_USER`
137
- `HERALD_SMTP_PASS`
138
- `HERALD_SMTP_FROM`
139
140
<p align="center">
141
<img src="https://raw.githubusercontent.com/taciturnaxolotl/carriage/main/.github/images/line-break.svg" />
···
1
# Herald ๐
2
3
+

4
5
This was inspired by the sunsetting of [pico.sh/feeds](https://blog.pico.sh/ann-033-moving-rss-to-email-pico-plus) being available outside of `pico+`. It is a totally understandable move from them as their email costs were skyrocketing and they needed to pay for it somehow. This was created to allow me to still get my rss feeds delivered to me each day by email which I have grown quite accustomed to. The config is completely compatible with the `pico.sh` format as of `2026-01-09` and should stay fairly stable. It is also configured over ssh with the slight addition that you can view your feeds on a website as well as I found myself wanting to hot load my feeds into my website :)
6
···
136
- `HERALD_SMTP_USER`
137
- `HERALD_SMTP_PASS`
138
- `HERALD_SMTP_FROM`
139
+
140
+
## Screenshots
141
+
142
+
here is an example of what an email digest looks like:
143
+
144
+

145
146
<p align="center">
147
<img src="https://raw.githubusercontent.com/taciturnaxolotl/carriage/main/.github/images/line-break.svg" />
+40
crush.json
+40
crush.json
···
···
1
+
{
2
+
"$schema": "https://charm.land/crush.json",
3
+
"lsp": {
4
+
"gopls": {
5
+
"options": {
6
+
"gofumpt": true,
7
+
"codelenses": {
8
+
"gc_details": true,
9
+
"generate": true,
10
+
"run_govulncheck": true,
11
+
"test": true,
12
+
"tidy": true,
13
+
"upgrade_dependency": true
14
+
},
15
+
"hints": {
16
+
"assignVariableTypes": true,
17
+
"compositeLiteralFields": true,
18
+
"compositeLiteralTypes": true,
19
+
"constantValues": true,
20
+
"functionTypeParameters": true,
21
+
"parameterNames": true,
22
+
"rangeVariableTypes": true
23
+
},
24
+
"analyses": {
25
+
"nilness": true,
26
+
"unusedparams": true,
27
+
"unusedvariable": true,
28
+
"unusedwrite": true,
29
+
"useany": true
30
+
},
31
+
"staticcheck": true,
32
+
"directoryFilters": [
33
+
"-.git",
34
+
"-node_modules"
35
+
],
36
+
"semanticTokens": true
37
+
}
38
+
}
39
+
}
40
+
}
+100
emoji_u1f38f.svg
+100
emoji_u1f38f.svg
···
···
1
+
<?xml version="1.0" encoding="utf-8"?>
2
+
<!-- Generator: Adobe Illustrator 25.2.3, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
3
+
<svg version="1.1" id="Layer_2" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
4
+
viewBox="0 0 128 128" style="enable-background:new 0 0 128 128;" xml:space="preserve">
5
+
<path style="fill:#FBC219;" d="M7.2,20.67l6.85-0.61l12.94,100.19c0,0-0.45,2.79-3.05,3.05c-3.05,0.3-3.81-2.59-3.81-2.59L7.2,20.67
6
+
z"/>
7
+
<path style="fill:#E07F16;" d="M2.67,17.47c0.63,5.16,5.25,6.72,8.34,6.51c3.43-0.23,7.88-2.88,7.31-8.45
8
+
c-0.67-6.54-6.4-7.77-9.6-6.97C5.07,9.47,2.1,12.79,2.67,17.47z"/>
9
+
<path style="fill:#FFC020;" d="M11.35,10.84C8.8,9.02,5.64,11.3,4.73,13.59S4.5,17.7,5.75,18.73s4,0.69,5.6-1.26
10
+
C12.85,15.65,13.75,12.56,11.35,10.84z"/>
11
+
<path style="fill:#016BA2;" d="M30.43,31.98c0,0,4.11-6.63,12.22-12s17.02-9.25,32.79-9.03c10.74,0.16,17.02,0.96,23.42,1.03
12
+
c10.62,0.11,17.2-3.89,18.85-1.26c2.28,3.66-6.34,14-6.17,17.02c0.23,4,11.54,2.74,11.65,6.97c0.08,2.86-6.28,10.05-15.99,12.45
13
+
S85.5,49,77.84,50.14c-7.65,1.14-21.14,5.5-25.53,6.9C45.9,59.09,30.43,31.98,30.43,31.98z"/>
14
+
<path style="fill:#DDE2E1;" d="M26,37.96l5.03-6.95c0,0,1.36,1,3.44,1.63c2.08,0.63,11.7,2.68,16.3,9.42
15
+
C55.85,49.49,52.82,57,52.82,57s-4.15,1.97-11.5,3.04c-4.76,0.69-7.79-0.17-7.79-0.17s-5.85-10.19-5.94-10.47S26,37.96,26,37.96z"/>
16
+
<path style="fill:#016BA2;" d="M42.56,39.08c-1.01-0.41-2.66,0.64-2.79,3.51c-0.24,5.17,5.35,6.42,5.35,6.42s0.66,2.8,2.56,3.98
17
+
c3.45,2.14,4.7,0.12,4.46-0.71c-0.24-0.83-1.43-0.77-2.5-2.2c-0.74-0.99-1.13-3.09-1.43-3.51s-2.5-0.18-4.22-3.09
18
+
C42.77,41.42,43.15,39.32,42.56,39.08z"/>
19
+
<path style="fill:#016BA2;" d="M27.22,37c-2.5,0.71-3.15,6.64-1.01,13.38c1.66,5.23,5.77,10.58,9.39,9.39
20
+
c3.15-1.03,4.04-6.66,1.19-13.85C33.94,38.73,30.29,36.12,27.22,37z"/>
21
+
<path style="fill:#DDE2E1;" d="M28.65,39.86c-1.04,0.26-0.59,6.78,0.71,9.87s3.92,7.37,5.41,6.72c0.76-0.34,0.65-5.41-0.71-9.33
22
+
C32.69,43.19,29.6,39.62,28.65,39.86z"/>
23
+
<path style="fill:#FFFFFF;" d="M53.64,25.39c-3.66,2.11-4.29,7.27-2.18,11.06c1.81,3.26,5.36,4.85,8.86,3.5
24
+
c3.37-1.3,4.79-6.38,2.89-10.63C61.4,25.28,56.77,23.59,53.64,25.39z"/>
25
+
<path style="fill:#2D2D2E;" d="M55.16,28.62c-2.21,1.31-2.27,4.03-1.14,6.03c1.24,2.19,3.99,2.19,5.46,1.28
26
+
c1.73-1.06,2.12-3.31,1.24-5.32C59.67,28.24,57.01,27.53,55.16,28.62z"/>
27
+
<path style="fill:#F26D31;" d="M37.9,75.09c0,0,4.35-6.95,14.41-10.58c15.3-5.52,29.65-4.43,40.23-4.67
28
+
c18.46-0.41,21.88-6.64,24.68-4.15c2.36,2.09-4.46,11.92-3.94,15.76c0.29,2.15,1.24,4.04,4.15,4.87s6.4,1.25,6.74,2.8
29
+
c0.41,1.87-5.08,7.88-15.55,11.92c-8.61,3.33-18.66,3.63-27.27,4.98c-7.28,1.14-15.1,3.63-19.39,4.98
30
+
c-5.6,1.76-10.26,2.07-10.26,2.07s-9.54-24.68-9.85-24.99C41.53,77.79,37.9,75.09,37.9,75.09z"/>
31
+
<path style="fill:#DDE2E1;" d="M38.1,74.51c0,0,6.12-0.37,11.59,3.55c5.08,3.64,10.91,10.83,10.66,17.51
32
+
c-0.14,3.81-2.4,6.52-2.4,6.52s-3.67,1.52-9.45,1.18c-7.18-0.42-9.22-3.05-9.22-3.05l-5.75-15.06c0,0,0.51-4.99,1.78-6.94
33
+
C36.58,76.29,38.1,74.51,38.1,74.51z"/>
34
+
<path style="fill:#F26D31;" d="M32.98,91.27c1.35,5.56,5.57,10.78,8.95,9.99c3.12-0.73,4.21-5.49,2.39-12.7
35
+
c-1.77-7.03-5-11.29-9.02-10.33C31.53,79.15,31.78,86.33,32.98,91.27z"/>
36
+
<path style="fill:#DDE2E1;" d="M36.16,81.49c-1.41,0.42-1.1,4.77-0.1,8.9c0.73,3.02,2.55,7.86,4.22,7.44
37
+
c1.67-0.42,1.09-5.67,0.31-8.69C39.8,86.12,37.89,80.97,36.16,81.49z"/>
38
+
<path style="fill:#F26D31;" d="M48.7,81.95c-2.13,0-1.62,4.38-0.26,6.56c1.82,2.91,4.06,3.54,4.06,3.54s0.49,2.79,2.19,4.68
39
+
c0.88,0.99,2.21,1.71,2.76,1.35c0.6-0.39-0.07-2.12-0.57-4.11c-0.68-2.65-0.78-4.27-0.78-4.27s-2.55-1.51-3.85-3.33
40
+
S50.79,81.95,48.7,81.95z"/>
41
+
<path style="fill:#F8FFFD;" d="M59.32,72.22c-3.5,2.13-3.64,7.13-0.94,11.09c2.71,3.96,7.08,3.8,9.42,2.45
42
+
c2.34-1.35,3.85-6.09,1.2-10.67C66.6,70.95,61.97,70.61,59.32,72.22z"/>
43
+
<path style="fill:#2D2D2E;" d="M61.61,74.77c-1.87,0.83-2.29,4.32-0.68,6.45c1.61,2.13,3.8,2.18,5,1.25
44
+
c1.87-1.46,1.57-4.11,0.68-5.72C65.56,74.88,63.31,74.02,61.61,74.77z"/>
45
+
<g>
46
+
<path style="fill:#B6CED5;" d="M13.96,56.39c-2.52,0-3.81-0.94-3.97-1.05c-0.66-0.51-0.78-1.45-0.27-2.1
47
+
c0.51-0.66,1.45-0.77,2.1-0.27l0,0c0,0,0.85,0.55,2.73,0.4c2.18-0.17,3.55-1.04,3.56-1.05l0.27-0.14l12.74-5.01
48
+
c0.77-0.3,1.64,0.08,1.95,0.85c0.3,0.77-0.08,1.64-0.85,1.95l-12.61,4.96c-0.53,0.32-2.26,1.24-4.83,1.44
49
+
C14.5,56.38,14.22,56.39,13.96,56.39z"/>
50
+
</g>
51
+
<g>
52
+
<path style="fill:#B6CED5;" d="M20.29,98.16c-2.62,0-4.37-0.89-4.56-0.99c-0.73-0.39-1.01-1.29-0.63-2.03
53
+
c0.39-0.73,1.29-1.01,2.03-0.63c0,0,1.28,0.64,3.16,0.64c1.62,0,2.87-1.4,2.88-1.42l0.27-0.31l0.4-0.14l14-4.63
54
+
c0.79-0.26,1.64,0.17,1.9,0.95c0.26,0.79-0.17,1.63-0.95,1.89l-13.65,4.51C24.44,96.71,22.67,98.16,20.29,98.16z"/>
55
+
</g>
56
+
<path style="fill:#39BDE3;" d="M73.1,35.27c-1.38,0.83,0.52,2.61-0.06,8.01c-0.5,4.7-4.17,8.84-4.17,8.84s2.4-0.67,3.72-0.94
57
+
c1.33-0.28,3.48-0.72,3.48-0.72s1.22-2.54,1.27-6.91C77.41,39.35,75.23,33.99,73.1,35.27z"/>
58
+
<path style="fill:#39BDE3;" d="M75.37,21.18c-0.17,0.31,0.44,1.88,0.83,3.43c0.39,1.55,0.64,3.68,1.11,3.87
59
+
c0.66,0.28,3.91-1.57,2.93-5.08C79.18,19.63,75.7,20.57,75.37,21.18z"/>
60
+
<path style="fill:#39BDE3;" d="M83.77,25.49c-0.77,0.88,0.33,2.04,0.72,3.59c0.39,1.55,0.48,3.18,0.94,3.37
61
+
c0.66,0.28,3.87-1.33,3.26-4.48C87.95,24.15,84.22,24.97,83.77,25.49z"/>
62
+
<path style="fill:#39BDE3;" d="M81.32,15.9c-0.48,0.97,0.52,2.02,0.9,4.01c0.3,1.57,0.53,3.56,0.99,3.76
63
+
c0.66,0.28,4.19-1.95,3.2-5.47C85.37,14.44,81.62,15.28,81.32,15.9z"/>
64
+
<path style="fill:#39BDE3;" d="M90.93,20.21c-0.17,0.31,0.44,1.88,0.83,3.43c0.39,1.55,0.64,3.68,1.11,3.87
65
+
c0.66,0.28,3.91-1.57,2.93-5.08C94.74,18.67,91.26,19.61,90.93,20.21z"/>
66
+
<path style="fill:#39BDE3;" d="M96.18,15.57c-0.15,0.32,0.52,2.51,0.9,4.06c0.39,1.55,0.75,3.29,1.22,3.48
67
+
c0.66,0.28,3.87-1.71,3.04-5.53C100.51,13.78,96.64,14.61,96.18,15.57z"/>
68
+
<path style="fill:#39BDE3;" d="M99.05,24.6c-0.19,0.33,0.64,2.42,1.07,4.04c0.43,1.63,0.59,3.38,1.1,3.62
69
+
c1,0.47,4.52-1.69,3.28-5.34C103.38,23.61,99.42,23.96,99.05,24.6z"/>
70
+
<path style="fill:#39BDE3;" d="M93,29.58c-0.17,0.34,0.66,1.84,1.16,3.7c0.45,1.68,0.67,4.01,1.13,4.22
71
+
c0.66,0.31,4.08-1.36,3.13-5.21C97.39,28.17,93.33,28.92,93,29.58z"/>
72
+
<path style="fill:#39BDE3;" d="M77.97,30.63c-0.17,0.31,0.39,2.21,0.77,3.76c0.39,1.55,0.62,3.32,1.09,3.51
73
+
c0.66,0.28,3.82-1.54,2.93-5.08C82,29.8,78.3,30.02,77.97,30.63z"/>
74
+
<path style="fill:#39BDE3;" d="M86.18,35.46c-0.48,0.86,0.41,1.91,0.79,3.45c0.39,1.55,0.69,3.62,1.14,3.84
75
+
c0.96,0.47,3.91-1.66,3.28-4.83C90.64,34.1,86.52,34.86,86.18,35.46z"/>
76
+
<path style="fill:#39BDE3;" d="M101.24,35.08c-0.17,0.3,0.42,2.19,0.93,3.67s1.08,3.17,1.56,3.32c0.68,0.22,4.11-2.36,2.92-5.75
77
+
C105.59,33.34,101.67,34.33,101.24,35.08z"/>
78
+
<path style="fill:#FEC516;" d="M80.7,77.14c-0.16,0.32,0.5,1.88,0.95,3.51c0.42,1.55,0.91,3.36,1.38,3.54
79
+
c0.67,0.26,3.87-1.67,2.81-5.18C84.7,75.26,81.02,76.52,80.7,77.14z"/>
80
+
<path style="fill:#FEC516;" d="M86.27,71.68c-0.17,0.31,0.79,2.03,1.31,3.45c0.55,1.5,0.64,3.68,1.11,3.87
81
+
c0.66,0.28,3.43-1.35,2.59-4.9C90.43,70.49,86.6,71.07,86.27,71.68z"/>
82
+
<path style="fill:#FEC516;" d="M78.11,67.53c-0.18,0.34,0.54,2,1,3.64c0.47,1.64,0.72,3.07,1.15,3.42c1.13,0.9,4.38-0.71,3.36-4.71
83
+
C82.59,65.82,78.46,66.87,78.11,67.53z"/>
84
+
<path style="fill:#FEC516;" d="M84.16,62.1c-0.66,0.47,0.32,1.9,0.6,3.47c0.28,1.57,0.3,3.4,0.77,3.58c1.35,0.51,4.27-0.3,3.55-4.45
85
+
C88.4,60.82,84.73,61.7,84.16,62.1z"/>
86
+
<path style="fill:#FEC516;" d="M92.23,66.89c-0.23,1.07,0.97,1.48,1.66,3.57c0.6,1.83,0.47,3.09,0.95,3.24
87
+
c1.46,0.45,4.65-2.1,3.47-5.12C96.91,64.97,92.38,66.17,92.23,66.89z"/>
88
+
<path style="fill:#FEC516;" d="M98.66,62.3c-0.17,0.31,0.33,1.38,1.02,3.26c0.55,1.5,0.64,3.68,1.11,3.87
89
+
c0.66,0.28,3.8-1.55,2.82-5.07C102.55,60.6,98.99,61.69,98.66,62.3z"/>
90
+
<path style="fill:#FEC516;" d="M101.52,71.6c-0.17,0.31,0.51,1.78,0.9,3.06c0.46,1.53,0.41,3.18,1.12,3.66
91
+
c1.07,0.73,4.17-1.48,3.04-4.96C105.54,70.13,101.86,70.99,101.52,71.6z"/>
92
+
<path style="fill:#FEC516;" d="M95.6,76.05c-0.17,0.31,0.38,1.91,0.64,3.5c0.28,1.77,0.27,3.34,0.72,3.55
93
+
c1.07,0.51,4.64-0.82,3.77-4.62C99.87,74.72,95.93,75.45,95.6,76.05z"/>
94
+
<path style="fill:#FEC516;" d="M103.86,80.66c-0.17,0.31,0.44,1.88,0.83,3.43c0.39,1.55,0.7,3,1.1,3.3c0.9,0.68,4.06-0.9,3.22-4.56
95
+
C108.13,79.02,104.19,80.05,103.86,80.66z"/>
96
+
<path style="fill:#FEC516;" d="M88.88,81.06c-0.48,0.65,0.6,2.13,0.79,3.72c0.23,1.86,0.28,3.27,0.73,3.49
97
+
c1.29,0.62,4.81-1.41,3.61-4.95C92.79,79.68,89.29,80.5,88.88,81.06z"/>
98
+
<path style="fill:#FEC516;" d="M72.8,97.85c0,0,3.47-3.86,3.39-6.75c-0.11-4.33-1.35-6.47,0.34-7.09c1.4-0.51,2.93,1.75,3.49,4.17
99
+
c1.35,5.82,0.06,8.1,0.06,8.1s-2.13,0.46-3.78,0.79C74.89,97.34,72.8,97.85,72.8,97.85z"/>
100
+
</svg>
+5
-1
flake.nix
+5
-1
flake.nix
···
29
{
30
default = pkgs.buildGoModule {
31
pname = "herald";
32
+
version = "0.1.1";
33
subPackages = [ "." ];
34
src = self;
35
vendorHash = "sha256-IE7JMJ4DehwYxrkh5YCgw7yWdybxtCCxWQO7M/u6bno=";
36
+
ldflags = [
37
+
"-X main.commitHash=${self.rev or self.dirtyRev or "dev"}"
38
+
"-X main.version=0.1.1"
39
+
];
40
};
41
}
42
);
+2
main.go
+2
main.go
+6
-6
scheduler/scheduler.go
+6
-6
scheduler/scheduler.go
···
179
}
180
}()
181
182
-
now := time.Now()
183
configs, err := s.store.GetDueConfigs(ctx, now)
184
if err != nil {
185
s.logger.Error("failed to get due configs", "err", err)
···
257
s.logger.Debug("RunNow: feed metadata updated")
258
259
s.logger.Debug("RunNow: calculating next run")
260
-
now := time.Now()
261
-
nextRun, err := gronx.NextTick(cfg.CronExpr, false)
262
if err != nil {
263
return stats, fmt.Errorf("calculate next run: %w", err)
264
}
···
277
func (s *Scheduler) collectNewItems(ctx context.Context, results []*FetchResult) ([]email.FeedGroup, int, error) {
278
var feedGroups []email.FeedGroup
279
totalNew := 0
280
-
maxAge := time.Now().Add(-itemMaxAge)
281
feedErrors := 0
282
283
for _, result := range results {
···
486
}
487
}
488
489
-
now := time.Now()
490
-
nextRun, err := gronx.NextTick(cfg.CronExpr, false)
491
if err != nil {
492
return fmt.Errorf("calculate next run: %w", err)
493
}
···
179
}
180
}()
181
182
+
now := time.Now().UTC()
183
configs, err := s.store.GetDueConfigs(ctx, now)
184
if err != nil {
185
s.logger.Error("failed to get due configs", "err", err)
···
257
s.logger.Debug("RunNow: feed metadata updated")
258
259
s.logger.Debug("RunNow: calculating next run")
260
+
now := time.Now().UTC()
261
+
nextRun, err := gronx.NextTick(cfg.CronExpr, true)
262
if err != nil {
263
return stats, fmt.Errorf("calculate next run: %w", err)
264
}
···
277
func (s *Scheduler) collectNewItems(ctx context.Context, results []*FetchResult) ([]email.FeedGroup, int, error) {
278
var feedGroups []email.FeedGroup
279
totalNew := 0
280
+
maxAge := time.Now().UTC().Add(-itemMaxAge)
281
feedErrors := 0
282
283
for _, result := range results {
···
486
}
487
}
488
489
+
now := time.Now().UTC()
490
+
nextRun, err := gronx.NextTick(cfg.CronExpr, true)
491
if err != nil {
492
return fmt.Errorf("calculate next run: %w", err)
493
}
+1
-1
ssh/commands.go
+1
-1
ssh/commands.go
+1
-1
ssh/scp.go
+1
-1
ssh/scp.go
+3
-3
store/configs.go
+3
-3
store/configs.go
···
49
InlineContent: inline,
50
RawText: rawText,
51
NextRun: sql.NullTime{Time: nextRun, Valid: true},
52
-
CreatedAt: time.Now(),
53
}, nil
54
}
55
···
78
InlineContent: inline,
79
RawText: rawText,
80
NextRun: sql.NullTime{Time: nextRun, Valid: true},
81
-
CreatedAt: time.Now(),
82
}, nil
83
}
84
···
219
return err
220
}
221
222
-
nextRun, err := gronx.NextTick(cfg.CronExpr, false)
223
if err != nil {
224
return fmt.Errorf("calculate next run: %w", err)
225
}
···
49
InlineContent: inline,
50
RawText: rawText,
51
NextRun: sql.NullTime{Time: nextRun, Valid: true},
52
+
CreatedAt: time.Now().UTC(),
53
}, nil
54
}
55
···
78
InlineContent: inline,
79
RawText: rawText,
80
NextRun: sql.NullTime{Time: nextRun, Valid: true},
81
+
CreatedAt: time.Now().UTC(),
82
}, nil
83
}
84
···
219
return err
220
}
221
222
+
nextRun, err := gronx.NextTick(cfg.CronExpr, true)
223
if err != nil {
224
return fmt.Errorf("calculate next run: %w", err)
225
}
+1
-1
store/users.go
+1
-1
store/users.go
+10
web/handlers.go
+10
web/handlers.go
···
60
_, _ = w.Write(css)
61
}
62
63
+
func (s *Server) handleFaviconSVG(w http.ResponseWriter, r *http.Request) {
64
+
svg, err := publicFS.ReadFile("public/favicon.svg")
65
+
if err != nil {
66
+
http.Error(w, "Not Found", http.StatusNotFound)
67
+
return
68
+
}
69
+
w.Header().Set("Content-Type", "image/svg+xml")
70
+
_, _ = w.Write(svg)
71
+
}
72
+
73
type userPageData struct {
74
Fingerprint string
75
ShortFingerprint string
+100
web/public/favicon.svg
+100
web/public/favicon.svg
···
···
1
+
<?xml version="1.0" encoding="utf-8"?>
2
+
<!-- Generator: Adobe Illustrator 25.2.3, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
3
+
<svg version="1.1" id="Layer_2" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
4
+
viewBox="0 0 128 128" style="enable-background:new 0 0 128 128;" xml:space="preserve">
5
+
<path style="fill:#FBC219;" d="M7.2,20.67l6.85-0.61l12.94,100.19c0,0-0.45,2.79-3.05,3.05c-3.05,0.3-3.81-2.59-3.81-2.59L7.2,20.67
6
+
z"/>
7
+
<path style="fill:#E07F16;" d="M2.67,17.47c0.63,5.16,5.25,6.72,8.34,6.51c3.43-0.23,7.88-2.88,7.31-8.45
8
+
c-0.67-6.54-6.4-7.77-9.6-6.97C5.07,9.47,2.1,12.79,2.67,17.47z"/>
9
+
<path style="fill:#FFC020;" d="M11.35,10.84C8.8,9.02,5.64,11.3,4.73,13.59S4.5,17.7,5.75,18.73s4,0.69,5.6-1.26
10
+
C12.85,15.65,13.75,12.56,11.35,10.84z"/>
11
+
<path style="fill:#016BA2;" d="M30.43,31.98c0,0,4.11-6.63,12.22-12s17.02-9.25,32.79-9.03c10.74,0.16,17.02,0.96,23.42,1.03
12
+
c10.62,0.11,17.2-3.89,18.85-1.26c2.28,3.66-6.34,14-6.17,17.02c0.23,4,11.54,2.74,11.65,6.97c0.08,2.86-6.28,10.05-15.99,12.45
13
+
S85.5,49,77.84,50.14c-7.65,1.14-21.14,5.5-25.53,6.9C45.9,59.09,30.43,31.98,30.43,31.98z"/>
14
+
<path style="fill:#DDE2E1;" d="M26,37.96l5.03-6.95c0,0,1.36,1,3.44,1.63c2.08,0.63,11.7,2.68,16.3,9.42
15
+
C55.85,49.49,52.82,57,52.82,57s-4.15,1.97-11.5,3.04c-4.76,0.69-7.79-0.17-7.79-0.17s-5.85-10.19-5.94-10.47S26,37.96,26,37.96z"/>
16
+
<path style="fill:#016BA2;" d="M42.56,39.08c-1.01-0.41-2.66,0.64-2.79,3.51c-0.24,5.17,5.35,6.42,5.35,6.42s0.66,2.8,2.56,3.98
17
+
c3.45,2.14,4.7,0.12,4.46-0.71c-0.24-0.83-1.43-0.77-2.5-2.2c-0.74-0.99-1.13-3.09-1.43-3.51s-2.5-0.18-4.22-3.09
18
+
C42.77,41.42,43.15,39.32,42.56,39.08z"/>
19
+
<path style="fill:#016BA2;" d="M27.22,37c-2.5,0.71-3.15,6.64-1.01,13.38c1.66,5.23,5.77,10.58,9.39,9.39
20
+
c3.15-1.03,4.04-6.66,1.19-13.85C33.94,38.73,30.29,36.12,27.22,37z"/>
21
+
<path style="fill:#DDE2E1;" d="M28.65,39.86c-1.04,0.26-0.59,6.78,0.71,9.87s3.92,7.37,5.41,6.72c0.76-0.34,0.65-5.41-0.71-9.33
22
+
C32.69,43.19,29.6,39.62,28.65,39.86z"/>
23
+
<path style="fill:#FFFFFF;" d="M53.64,25.39c-3.66,2.11-4.29,7.27-2.18,11.06c1.81,3.26,5.36,4.85,8.86,3.5
24
+
c3.37-1.3,4.79-6.38,2.89-10.63C61.4,25.28,56.77,23.59,53.64,25.39z"/>
25
+
<path style="fill:#2D2D2E;" d="M55.16,28.62c-2.21,1.31-2.27,4.03-1.14,6.03c1.24,2.19,3.99,2.19,5.46,1.28
26
+
c1.73-1.06,2.12-3.31,1.24-5.32C59.67,28.24,57.01,27.53,55.16,28.62z"/>
27
+
<path style="fill:#F26D31;" d="M37.9,75.09c0,0,4.35-6.95,14.41-10.58c15.3-5.52,29.65-4.43,40.23-4.67
28
+
c18.46-0.41,21.88-6.64,24.68-4.15c2.36,2.09-4.46,11.92-3.94,15.76c0.29,2.15,1.24,4.04,4.15,4.87s6.4,1.25,6.74,2.8
29
+
c0.41,1.87-5.08,7.88-15.55,11.92c-8.61,3.33-18.66,3.63-27.27,4.98c-7.28,1.14-15.1,3.63-19.39,4.98
30
+
c-5.6,1.76-10.26,2.07-10.26,2.07s-9.54-24.68-9.85-24.99C41.53,77.79,37.9,75.09,37.9,75.09z"/>
31
+
<path style="fill:#DDE2E1;" d="M38.1,74.51c0,0,6.12-0.37,11.59,3.55c5.08,3.64,10.91,10.83,10.66,17.51
32
+
c-0.14,3.81-2.4,6.52-2.4,6.52s-3.67,1.52-9.45,1.18c-7.18-0.42-9.22-3.05-9.22-3.05l-5.75-15.06c0,0,0.51-4.99,1.78-6.94
33
+
C36.58,76.29,38.1,74.51,38.1,74.51z"/>
34
+
<path style="fill:#F26D31;" d="M32.98,91.27c1.35,5.56,5.57,10.78,8.95,9.99c3.12-0.73,4.21-5.49,2.39-12.7
35
+
c-1.77-7.03-5-11.29-9.02-10.33C31.53,79.15,31.78,86.33,32.98,91.27z"/>
36
+
<path style="fill:#DDE2E1;" d="M36.16,81.49c-1.41,0.42-1.1,4.77-0.1,8.9c0.73,3.02,2.55,7.86,4.22,7.44
37
+
c1.67-0.42,1.09-5.67,0.31-8.69C39.8,86.12,37.89,80.97,36.16,81.49z"/>
38
+
<path style="fill:#F26D31;" d="M48.7,81.95c-2.13,0-1.62,4.38-0.26,6.56c1.82,2.91,4.06,3.54,4.06,3.54s0.49,2.79,2.19,4.68
39
+
c0.88,0.99,2.21,1.71,2.76,1.35c0.6-0.39-0.07-2.12-0.57-4.11c-0.68-2.65-0.78-4.27-0.78-4.27s-2.55-1.51-3.85-3.33
40
+
S50.79,81.95,48.7,81.95z"/>
41
+
<path style="fill:#F8FFFD;" d="M59.32,72.22c-3.5,2.13-3.64,7.13-0.94,11.09c2.71,3.96,7.08,3.8,9.42,2.45
42
+
c2.34-1.35,3.85-6.09,1.2-10.67C66.6,70.95,61.97,70.61,59.32,72.22z"/>
43
+
<path style="fill:#2D2D2E;" d="M61.61,74.77c-1.87,0.83-2.29,4.32-0.68,6.45c1.61,2.13,3.8,2.18,5,1.25
44
+
c1.87-1.46,1.57-4.11,0.68-5.72C65.56,74.88,63.31,74.02,61.61,74.77z"/>
45
+
<g>
46
+
<path style="fill:#B6CED5;" d="M13.96,56.39c-2.52,0-3.81-0.94-3.97-1.05c-0.66-0.51-0.78-1.45-0.27-2.1
47
+
c0.51-0.66,1.45-0.77,2.1-0.27l0,0c0,0,0.85,0.55,2.73,0.4c2.18-0.17,3.55-1.04,3.56-1.05l0.27-0.14l12.74-5.01
48
+
c0.77-0.3,1.64,0.08,1.95,0.85c0.3,0.77-0.08,1.64-0.85,1.95l-12.61,4.96c-0.53,0.32-2.26,1.24-4.83,1.44
49
+
C14.5,56.38,14.22,56.39,13.96,56.39z"/>
50
+
</g>
51
+
<g>
52
+
<path style="fill:#B6CED5;" d="M20.29,98.16c-2.62,0-4.37-0.89-4.56-0.99c-0.73-0.39-1.01-1.29-0.63-2.03
53
+
c0.39-0.73,1.29-1.01,2.03-0.63c0,0,1.28,0.64,3.16,0.64c1.62,0,2.87-1.4,2.88-1.42l0.27-0.31l0.4-0.14l14-4.63
54
+
c0.79-0.26,1.64,0.17,1.9,0.95c0.26,0.79-0.17,1.63-0.95,1.89l-13.65,4.51C24.44,96.71,22.67,98.16,20.29,98.16z"/>
55
+
</g>
56
+
<path style="fill:#39BDE3;" d="M73.1,35.27c-1.38,0.83,0.52,2.61-0.06,8.01c-0.5,4.7-4.17,8.84-4.17,8.84s2.4-0.67,3.72-0.94
57
+
c1.33-0.28,3.48-0.72,3.48-0.72s1.22-2.54,1.27-6.91C77.41,39.35,75.23,33.99,73.1,35.27z"/>
58
+
<path style="fill:#39BDE3;" d="M75.37,21.18c-0.17,0.31,0.44,1.88,0.83,3.43c0.39,1.55,0.64,3.68,1.11,3.87
59
+
c0.66,0.28,3.91-1.57,2.93-5.08C79.18,19.63,75.7,20.57,75.37,21.18z"/>
60
+
<path style="fill:#39BDE3;" d="M83.77,25.49c-0.77,0.88,0.33,2.04,0.72,3.59c0.39,1.55,0.48,3.18,0.94,3.37
61
+
c0.66,0.28,3.87-1.33,3.26-4.48C87.95,24.15,84.22,24.97,83.77,25.49z"/>
62
+
<path style="fill:#39BDE3;" d="M81.32,15.9c-0.48,0.97,0.52,2.02,0.9,4.01c0.3,1.57,0.53,3.56,0.99,3.76
63
+
c0.66,0.28,4.19-1.95,3.2-5.47C85.37,14.44,81.62,15.28,81.32,15.9z"/>
64
+
<path style="fill:#39BDE3;" d="M90.93,20.21c-0.17,0.31,0.44,1.88,0.83,3.43c0.39,1.55,0.64,3.68,1.11,3.87
65
+
c0.66,0.28,3.91-1.57,2.93-5.08C94.74,18.67,91.26,19.61,90.93,20.21z"/>
66
+
<path style="fill:#39BDE3;" d="M96.18,15.57c-0.15,0.32,0.52,2.51,0.9,4.06c0.39,1.55,0.75,3.29,1.22,3.48
67
+
c0.66,0.28,3.87-1.71,3.04-5.53C100.51,13.78,96.64,14.61,96.18,15.57z"/>
68
+
<path style="fill:#39BDE3;" d="M99.05,24.6c-0.19,0.33,0.64,2.42,1.07,4.04c0.43,1.63,0.59,3.38,1.1,3.62
69
+
c1,0.47,4.52-1.69,3.28-5.34C103.38,23.61,99.42,23.96,99.05,24.6z"/>
70
+
<path style="fill:#39BDE3;" d="M93,29.58c-0.17,0.34,0.66,1.84,1.16,3.7c0.45,1.68,0.67,4.01,1.13,4.22
71
+
c0.66,0.31,4.08-1.36,3.13-5.21C97.39,28.17,93.33,28.92,93,29.58z"/>
72
+
<path style="fill:#39BDE3;" d="M77.97,30.63c-0.17,0.31,0.39,2.21,0.77,3.76c0.39,1.55,0.62,3.32,1.09,3.51
73
+
c0.66,0.28,3.82-1.54,2.93-5.08C82,29.8,78.3,30.02,77.97,30.63z"/>
74
+
<path style="fill:#39BDE3;" d="M86.18,35.46c-0.48,0.86,0.41,1.91,0.79,3.45c0.39,1.55,0.69,3.62,1.14,3.84
75
+
c0.96,0.47,3.91-1.66,3.28-4.83C90.64,34.1,86.52,34.86,86.18,35.46z"/>
76
+
<path style="fill:#39BDE3;" d="M101.24,35.08c-0.17,0.3,0.42,2.19,0.93,3.67s1.08,3.17,1.56,3.32c0.68,0.22,4.11-2.36,2.92-5.75
77
+
C105.59,33.34,101.67,34.33,101.24,35.08z"/>
78
+
<path style="fill:#FEC516;" d="M80.7,77.14c-0.16,0.32,0.5,1.88,0.95,3.51c0.42,1.55,0.91,3.36,1.38,3.54
79
+
c0.67,0.26,3.87-1.67,2.81-5.18C84.7,75.26,81.02,76.52,80.7,77.14z"/>
80
+
<path style="fill:#FEC516;" d="M86.27,71.68c-0.17,0.31,0.79,2.03,1.31,3.45c0.55,1.5,0.64,3.68,1.11,3.87
81
+
c0.66,0.28,3.43-1.35,2.59-4.9C90.43,70.49,86.6,71.07,86.27,71.68z"/>
82
+
<path style="fill:#FEC516;" d="M78.11,67.53c-0.18,0.34,0.54,2,1,3.64c0.47,1.64,0.72,3.07,1.15,3.42c1.13,0.9,4.38-0.71,3.36-4.71
83
+
C82.59,65.82,78.46,66.87,78.11,67.53z"/>
84
+
<path style="fill:#FEC516;" d="M84.16,62.1c-0.66,0.47,0.32,1.9,0.6,3.47c0.28,1.57,0.3,3.4,0.77,3.58c1.35,0.51,4.27-0.3,3.55-4.45
85
+
C88.4,60.82,84.73,61.7,84.16,62.1z"/>
86
+
<path style="fill:#FEC516;" d="M92.23,66.89c-0.23,1.07,0.97,1.48,1.66,3.57c0.6,1.83,0.47,3.09,0.95,3.24
87
+
c1.46,0.45,4.65-2.1,3.47-5.12C96.91,64.97,92.38,66.17,92.23,66.89z"/>
88
+
<path style="fill:#FEC516;" d="M98.66,62.3c-0.17,0.31,0.33,1.38,1.02,3.26c0.55,1.5,0.64,3.68,1.11,3.87
89
+
c0.66,0.28,3.8-1.55,2.82-5.07C102.55,60.6,98.99,61.69,98.66,62.3z"/>
90
+
<path style="fill:#FEC516;" d="M101.52,71.6c-0.17,0.31,0.51,1.78,0.9,3.06c0.46,1.53,0.41,3.18,1.12,3.66
91
+
c1.07,0.73,4.17-1.48,3.04-4.96C105.54,70.13,101.86,70.99,101.52,71.6z"/>
92
+
<path style="fill:#FEC516;" d="M95.6,76.05c-0.17,0.31,0.38,1.91,0.64,3.5c0.28,1.77,0.27,3.34,0.72,3.55
93
+
c1.07,0.51,4.64-0.82,3.77-4.62C99.87,74.72,95.93,75.45,95.6,76.05z"/>
94
+
<path style="fill:#FEC516;" d="M103.86,80.66c-0.17,0.31,0.44,1.88,0.83,3.43c0.39,1.55,0.7,3,1.1,3.3c0.9,0.68,4.06-0.9,3.22-4.56
95
+
C108.13,79.02,104.19,80.05,103.86,80.66z"/>
96
+
<path style="fill:#FEC516;" d="M88.88,81.06c-0.48,0.65,0.6,2.13,0.79,3.72c0.23,1.86,0.28,3.27,0.73,3.49
97
+
c1.29,0.62,4.81-1.41,3.61-4.95C92.79,79.68,89.29,80.5,88.88,81.06z"/>
98
+
<path style="fill:#FEC516;" d="M72.8,97.85c0,0,3.47-3.86,3.39-6.75c-0.11-4.33-1.35-6.47,0.34-7.09c1.4-0.51,2.93,1.75,3.49,4.17
99
+
c1.35,5.82,0.06,8.1,0.06,8.1s-2.13,0.46-3.78,0.79C74.89,97.34,72.8,97.85,72.8,97.85z"/>
100
+
</svg>
+4
web/server.go
+4
web/server.go
···
18
//go:embed templates/*
19
var templatesFS embed.FS
20
21
const (
22
// HTTP rate limiting
23
httpRequestsPerSecond = 10
···
56
57
mux.HandleFunc("/", s.routeHandler)
58
mux.HandleFunc("/style.css", s.handleStyleCSS)
59
mux.HandleFunc("/health", s.handleHealth)
60
mux.HandleFunc("/metrics", s.handleMetrics)
61
···
18
//go:embed templates/*
19
var templatesFS embed.FS
20
21
+
//go:embed public/*
22
+
var publicFS embed.FS
23
+
24
const (
25
// HTTP rate limiting
26
httpRequestsPerSecond = 10
···
59
60
mux.HandleFunc("/", s.routeHandler)
61
mux.HandleFunc("/style.css", s.handleStyleCSS)
62
+
mux.HandleFunc("/favicon.svg", s.handleFaviconSVG)
63
mux.HandleFunc("/health", s.handleHealth)
64
mux.HandleFunc("/metrics", s.handleMetrics)
65
+1
-1
web/templates/404.html
+1
-1
web/templates/404.html
···
4
<meta charset="utf-8">
5
<meta name="viewport" content="width=device-width, initial-scale=1">
6
<title>HERALD - {{if .Title}}{{.Title}}{{else}}404{{end}}</title>
7
-
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>๐</text></svg>">
8
<link rel="stylesheet" href="/style.css">
9
</head>
10
<body>
+37
-2
web/templates/index.html
+37
-2
web/templates/index.html
···
3
<head>
4
<meta charset="utf-8">
5
<meta name="viewport" content="width=device-width, initial-scale=1">
6
-
<title>HERALD</title>
7
-
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>๐</text></svg>">
8
<link rel="stylesheet" href="/style.css">
9
</head>
10
<body>
···
13
<h2>NAME</h2>
14
<pre>
15
herald - RSS-to-Email via SSH
16
</pre>
17
18
<h2>COMMANDS</h2>
···
3
<head>
4
<meta charset="utf-8">
5
<meta name="viewport" content="width=device-width, initial-scale=1">
6
+
<title>HERALD - RSS-to-Email via SSH</title>
7
+
<meta name="description" content="Herald delivers RSS feeds to your inbox via SSH. Upload feed configs, manage subscriptions, and receive updates by email.">
8
+
9
+
<!-- Open Graph -->
10
+
<meta property="og:type" content="website">
11
+
<meta property="og:title" content="HERALD - RSS-to-Email via SSH">
12
+
<meta property="og:description" content="Herald delivers RSS feeds to your inbox via SSH. Upload feed configs, manage subscriptions, and receive updates by email.">
13
+
<meta property="og:url" content="{{.Origin}}">
14
+
<meta property="og:image" content="https://l4.dunkirk.sh/i/AQ0z6BnQAwWu.webp">
15
+
<meta property="og:site_name" content="HERALD">
16
+
17
+
<!-- Twitter Card -->
18
+
<meta name="twitter:card" content="summary_large_image">
19
+
<meta name="twitter:title" content="HERALD - RSS-to-Email via SSH">
20
+
<meta name="twitter:description" content="Herald delivers RSS feeds to your inbox via SSH. Upload feed configs, manage subscriptions, and receive updates by email.">
21
+
<meta name="twitter:image" content="https://l4.dunkirk.sh/i/AQ0z6BnQAwWu.webp">
22
+
23
+
<link rel="icon" href="/favicon.svg" type="image/svg+xml">
24
<link rel="stylesheet" href="/style.css">
25
</head>
26
<body>
···
29
<h2>NAME</h2>
30
<pre>
31
herald - RSS-to-Email via SSH
32
+
</pre>
33
+
34
+
<h2>FILE FORMAT</h2>
35
+
<pre>
36
+
Feed configs are plain text files with directives:
37
+
38
+
=: email you@example.com Recipient email address
39
+
=: cron 0 8 * * * Standard cron (5 fields)
40
+
=: digest true Combine items (default: true)
41
+
=: inline false Include content (default: false)
42
+
=> https://example.com/feed RSS/Atom feed URL
43
+
=> https://blog.com/rss "Blog" Feed with display name
44
+
45
+
Example feeds.txt:
46
+
=: email you@example.com
47
+
=: cron 0 8 * * *
48
+
=: digest true
49
+
=> https://dunkirk.sh/atom.xml
50
+
=> https://news.ycombinator.com/rss
51
</pre>
52
53
<h2>COMMANDS</h2>
+1
-1
web/templates/keepalive.html
+1
-1
web/templates/keepalive.html
···
4
<meta charset="utf-8">
5
<meta name="viewport" content="width=device-width, initial-scale=1">
6
<title>HERALD - Digest Active</title>
7
-
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>๐</text></svg>">
8
<link rel="stylesheet" href="/style.css">
9
</head>
10
<body>
+1
-1
web/templates/unsubscribe.html
+1
-1
web/templates/unsubscribe.html
···
4
<meta charset="utf-8">
5
<meta name="viewport" content="width=device-width, initial-scale=1">
6
<title>HERALD - Unsubscribe</title>
7
-
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>๐</text></svg>">
8
<link rel="stylesheet" href="/style.css">
9
</head>
10
<body>
+17
-1
web/templates/user.html
+17
-1
web/templates/user.html
···
4
<meta charset="utf-8">
5
<meta name="viewport" content="width=device-width, initial-scale=1">
6
<title>HERALD - {{.ShortFingerprint}}</title>
7
-
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>๐</text></svg>">
8
<link rel="stylesheet" href="/style.css">
9
</head>
10
<body>
···
4
<meta charset="utf-8">
5
<meta name="viewport" content="width=device-width, initial-scale=1">
6
<title>HERALD - {{.ShortFingerprint}}</title>
7
+
<meta name="description" content="Herald dashboard for user {{.ShortFingerprint}}. View feed configs and delivery status.">
8
+
9
+
<!-- Open Graph -->
10
+
<meta property="og:type" content="website">
11
+
<meta property="og:title" content="HERALD - {{.ShortFingerprint}}">
12
+
<meta property="og:description" content="Herald dashboard for user {{.ShortFingerprint}}. View feed configs and delivery status.">
13
+
<meta property="og:url" content="{{.Origin}}/{{.Fingerprint}}">
14
+
<meta property="og:image" content="https://l4.dunkirk.sh/i/AQ0z6BnQAwWu.webp">
15
+
<meta property="og:site_name" content="HERALD">
16
+
17
+
<!-- Twitter Card -->
18
+
<meta name="twitter:card" content="summary_large_image">
19
+
<meta name="twitter:title" content="HERALD - {{.ShortFingerprint}}">
20
+
<meta name="twitter:description" content="Herald dashboard for user {{.ShortFingerprint}}. View feed configs and delivery status.">
21
+
<meta name="twitter:image" content="https://l4.dunkirk.sh/i/AQ0z6BnQAwWu.webp">
22
+
23
+
<link rel="icon" href="/favicon.svg" type="image/svg+xml">
24
<link rel="stylesheet" href="/style.css">
25
</head>
26
<body>