rss email digests over ssh because you're a cool kid herald.dunkirk.sh
go rss rss-reader ssh charm

Compare changes

Choose any two refs to compare.

+7 -1
README.md
··· 1 # Herald ๐ŸŽ 2 3 - ![email from harold](https://l4.dunkirk.sh/i/Ck271POS5n0k.webp) 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 + ![herald web interface](https://l4.dunkirk.sh/i/TuJ7meLh1JB9.webp) 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 + ![email from harold](https://l4.dunkirk.sh/i/Ck271POS5n0k.webp) 145 146 <p align="center"> 147 <img src="https://raw.githubusercontent.com/taciturnaxolotl/carriage/main/.github/images/line-break.svg" />
+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
···
··· 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>
+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
··· 288 } 289 290 func formatRelativeTime(t time.Time) string { 291 - now := time.Now() 292 diff := t.Sub(now) 293 294 if diff < 0 {
··· 288 } 289 290 func formatRelativeTime(t time.Time) string { 291 + now := time.Now().UTC() 292 diff := t.Sub(now) 293 294 if diff < 0 {
+1 -1
ssh/scp.go
··· 182 } 183 184 func calculateNextRun(cronExpr string) (time.Time, error) { 185 - return gronx.NextTick(cronExpr, false) 186 } 187 188 type configFileInfo struct {
··· 182 } 183 184 func calculateNextRun(cronExpr string) (time.Time, error) { 185 + return gronx.NextTick(cronExpr, true) 186 } 187 188 type configFileInfo struct {
+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
··· 42 ID: id, 43 PubkeyFP: pubkeyFP, 44 Pubkey: pubkey, 45 - CreatedAt: time.Now(), 46 }, nil 47 } 48
··· 42 ID: id, 43 PubkeyFP: pubkeyFP, 44 Pubkey: pubkey, 45 + CreatedAt: time.Now().UTC(), 46 }, nil 47 } 48
+10
web/handlers.go
··· 60 _, _ = w.Write(css) 61 } 62 63 type userPageData struct { 64 Fingerprint string 65 ShortFingerprint string
··· 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
···
··· 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
··· 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
··· 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>
··· 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="/favicon.svg" type="image/svg+xml"> 8 <link rel="stylesheet" href="/style.css"> 9 </head> 10 <body>
+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 + =&gt; https://example.com/feed RSS/Atom feed URL 43 + =&gt; 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 + =&gt; https://dunkirk.sh/atom.xml 50 + =&gt; https://news.ycombinator.com/rss 51 </pre> 52 53 <h2>COMMANDS</h2>
+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>
··· 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="/favicon.svg" type="image/svg+xml"> 8 <link rel="stylesheet" href="/style.css"> 9 </head> 10 <body>
+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>
··· 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="/favicon.svg" type="image/svg+xml"> 8 <link rel="stylesheet" href="/style.css"> 9 </head> 10 <body>
+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>