Fix push-digest sentinel filter, watermark, and cleanups from review
- push digest queries now exclude _duplicate channel like every other visibility query (bookmarks list/count and NewClassifiedSince) - advance push watermark to newest scanned story, not pass-start now, so stories arriving during a long send pass aren't re-counted next pass - replace hand-rolled escapeHTMLText with stdlib html.EscapeString - drop em-dashes from user-facing copy; bump PWA CACHE_VERSION so clients pick up the changed shell assets
This commit is contained in:
@@ -96,7 +96,7 @@ func NewClassifiedSince(sinceUnix int64, limit int) ([]Story, error) {
|
|||||||
rows, err := Get().Query(
|
rows, err := Get().Query(
|
||||||
`SELECT id, headline, source, channel, seen_at
|
`SELECT id, headline, source, channel, seen_at
|
||||||
FROM stories
|
FROM stories
|
||||||
WHERE classified = 1 AND channel <> '_discarded' AND seen_at > ?
|
WHERE classified = 1 AND channel NOT IN ('_discarded', '_duplicate') AND seen_at > ?
|
||||||
ORDER BY seen_at DESC
|
ORDER BY seen_at DESC
|
||||||
LIMIT ?`, sinceUnix, limit)
|
LIMIT ?`, sinceUnix, limit)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -104,6 +104,7 @@ func ListBookmarks(sub string, limit, offset int) ([]Story, error) {
|
|||||||
WHERE u.user_sub = ?
|
WHERE u.user_sub = ?
|
||||||
AND u.bookmarked_at IS NOT NULL
|
AND u.bookmarked_at IS NOT NULL
|
||||||
AND s.classified = 1
|
AND s.classified = 1
|
||||||
|
AND s.channel NOT IN ('_discarded', '_duplicate')
|
||||||
ORDER BY u.bookmarked_at DESC, u.story_id DESC
|
ORDER BY u.bookmarked_at DESC, u.story_id DESC
|
||||||
LIMIT ? OFFSET ?`, sub, limit, offset)
|
LIMIT ? OFFSET ?`, sub, limit, offset)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -129,7 +130,8 @@ func CountBookmarks(sub string) (int, error) {
|
|||||||
err := Get().QueryRow(
|
err := Get().QueryRow(
|
||||||
`SELECT COUNT(*) FROM user_story_state u
|
`SELECT COUNT(*) FROM user_story_state u
|
||||||
JOIN stories s ON s.id = u.story_id
|
JOIN stories s ON s.id = u.story_id
|
||||||
WHERE u.user_sub = ? AND u.bookmarked_at IS NOT NULL AND s.classified = 1`,
|
WHERE u.user_sub = ? AND u.bookmarked_at IS NOT NULL AND s.classified = 1
|
||||||
|
AND s.channel NOT IN ('_discarded', '_duplicate')`,
|
||||||
sub).Scan(&n)
|
sub).Scan(&n)
|
||||||
return n, err
|
return n, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package web
|
|||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"encoding/xml"
|
"encoding/xml"
|
||||||
|
"html"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -284,20 +285,8 @@ func contentToHTML(text string) string {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
b.WriteString("<p>")
|
b.WriteString("<p>")
|
||||||
b.WriteString(strings.ReplaceAll(escapeHTMLText(para), "\n", "<br>"))
|
b.WriteString(strings.ReplaceAll(html.EscapeString(para), "\n", "<br>"))
|
||||||
b.WriteString("</p>")
|
b.WriteString("</p>")
|
||||||
}
|
}
|
||||||
return b.String()
|
return b.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
// escapeHTMLText escapes the five characters that matter inside HTML text so the
|
|
||||||
// article body can't inject markup into content:encoded.
|
|
||||||
func escapeHTMLText(s string) string {
|
|
||||||
return strings.NewReplacer(
|
|
||||||
"&", "&",
|
|
||||||
"<", "<",
|
|
||||||
">", ">",
|
|
||||||
`"`, """,
|
|
||||||
"'", "'",
|
|
||||||
).Replace(s)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -56,7 +56,6 @@ func (s *Server) sendDigests() {
|
|||||||
if len(subs) == 0 {
|
if len(subs) == 0 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
now := time.Now().Unix()
|
|
||||||
// Disabled-source sets are per user; cache within a pass so a user with
|
// Disabled-source sets are per user; cache within a pass so a user with
|
||||||
// several devices only parses prefs once.
|
// several devices only parses prefs once.
|
||||||
disabledByUser := make(map[string]map[string]bool)
|
disabledByUser := make(map[string]map[string]bool)
|
||||||
@@ -116,7 +115,11 @@ func (s *Server) sendDigests() {
|
|||||||
slog.Warn("push: send failed", "sub", sub.UserSub, "err", err)
|
slog.Warn("push: send failed", "sub", sub.UserSub, "err", err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if derr := storage.TouchPushSubscription(sub.Endpoint, now); derr != nil {
|
// Advance to the newest story this digest actually accounted for, not a
|
||||||
|
// pass-start "now": the loop can run for minutes (one slow endpoint per
|
||||||
|
// send), so stories arriving mid-pass would otherwise be re-counted next
|
||||||
|
// pass. stories[0] is the newest in the scanned window (seen_at DESC).
|
||||||
|
if derr := storage.TouchPushSubscription(sub.Endpoint, stories[0].SeenAt); derr != nil {
|
||||||
slog.Error("push: advance watermark failed", "err", derr)
|
slog.Error("push: advance watermark failed", "err", derr)
|
||||||
}
|
}
|
||||||
sent++
|
sent++
|
||||||
@@ -133,7 +136,7 @@ func buildDigestPayload(count int, top string) []byte {
|
|||||||
body = "1 new story"
|
body = "1 new story"
|
||||||
}
|
}
|
||||||
if top != "" {
|
if top != "" {
|
||||||
body += " — " + top
|
body += ": " + top
|
||||||
}
|
}
|
||||||
b, _ := json.Marshal(map[string]string{
|
b, _ := json.Marshal(map[string]string{
|
||||||
"title": "Pete has fresh news",
|
"title": "Pete has fresh news",
|
||||||
|
|||||||
@@ -101,7 +101,7 @@
|
|||||||
if (sub) return unsubscribe().then(function () { paint(false, "Notifications off."); });
|
if (sub) return unsubscribe().then(function () { paint(false, "Notifications off."); });
|
||||||
return Notification.requestPermission().then(function (perm) {
|
return Notification.requestPermission().then(function (perm) {
|
||||||
if (perm !== "granted") { paint(false, "Permission denied."); return; }
|
if (perm !== "granted") { paint(false, "Permission denied."); return; }
|
||||||
return subscribe().then(function () { paint(true, "You're all set — new stories will nudge you."); });
|
return subscribe().then(function () { paint(true, "You're all set. New stories will nudge you."); });
|
||||||
});
|
});
|
||||||
}).catch(function () {
|
}).catch(function () {
|
||||||
paint(false, "Something went wrong. Try again.");
|
paint(false, "Something went wrong. Try again.");
|
||||||
|
|||||||
@@ -242,7 +242,7 @@
|
|||||||
: "";
|
: "";
|
||||||
var href = safeURL(it.url);
|
var href = safeURL(it.url);
|
||||||
var note = '<p class="pete-reader-note">' +
|
var note = '<p class="pete-reader-note">' +
|
||||||
(it.paywalled ? "This source is paywalled — the text above may be partial. " : "") +
|
(it.paywalled ? "This source is paywalled, so the text above may be partial. " : "") +
|
||||||
(href ? 'Read it at the source: <a href="' + escapeHTML(href) + '" target="_blank" rel="noopener noreferrer">' +
|
(href ? 'Read it at the source: <a href="' + escapeHTML(href) + '" target="_blank" rel="noopener noreferrer">' +
|
||||||
escapeHTML(it.source || "original article") + " ↗</a>." : "") +
|
escapeHTML(it.source || "original article") + " ↗</a>." : "") +
|
||||||
"</p>";
|
"</p>";
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
//
|
//
|
||||||
// Bump CACHE_VERSION whenever the precached shell assets change; activate()
|
// Bump CACHE_VERSION whenever the precached shell assets change; activate()
|
||||||
// drops every cache that doesn't match the current version.
|
// drops every cache that doesn't match the current version.
|
||||||
var CACHE_VERSION = "v2";
|
var CACHE_VERSION = "v3";
|
||||||
var SHELL_CACHE = "pete-shell-" + CACHE_VERSION;
|
var SHELL_CACHE = "pete-shell-" + CACHE_VERSION;
|
||||||
var RUNTIME_CACHE = "pete-runtime-" + CACHE_VERSION;
|
var RUNTIME_CACHE = "pete-runtime-" + CACHE_VERSION;
|
||||||
|
|
||||||
@@ -77,7 +77,7 @@ function offlineFallback() {
|
|||||||
"<div style=\"font-family:system-ui,sans-serif;max-width:32rem;margin:20vh auto;padding:0 1.5rem;text-align:center;color:#3a2f1a\">" +
|
"<div style=\"font-family:system-ui,sans-serif;max-width:32rem;margin:20vh auto;padding:0 1.5rem;text-align:center;color:#3a2f1a\">" +
|
||||||
"<div style=font-size:3rem>🦆</div>" +
|
"<div style=font-size:3rem>🦆</div>" +
|
||||||
"<h1 style=font-size:1.4rem>You're offline</h1>" +
|
"<h1 style=font-size:1.4rem>You're offline</h1>" +
|
||||||
"<p style=opacity:.7>Pete can't reach the news right now. Articles you've already opened are still readable — head back and try one of those.</p>" +
|
"<p style=opacity:.7>Pete can't reach the news right now. Articles you've already opened are still readable, so head back and try one of those.</p>" +
|
||||||
"</div>",
|
"</div>",
|
||||||
{ headers: { "Content-Type": "text/html; charset=utf-8" }, status: 503 }
|
{ headers: { "Content-Type": "text/html; charset=utf-8" }, status: 503 }
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user