@rmdes/indiekit-endpoint-microsub 1.0.55 → 1.0.57
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/assets/reader.js +408 -0
- package/index.js +61 -49
- package/lib/activitypub/outbox-fetcher.js +14 -2
- package/lib/cache/redis.js +26 -7
- package/lib/controllers/channels.js +2 -2
- package/lib/controllers/reader/actor.js +142 -0
- package/lib/controllers/reader/channel.js +301 -0
- package/lib/controllers/reader/compose.js +242 -0
- package/lib/controllers/reader/deck.js +129 -0
- package/lib/controllers/reader/feed-repair.js +117 -0
- package/lib/controllers/reader/feed.js +246 -0
- package/lib/controllers/reader/index.js +126 -0
- package/lib/controllers/reader/search.js +157 -0
- package/lib/controllers/reader/timeline.js +250 -0
- package/lib/controllers/search.js +6 -0
- package/lib/controllers/timeline.js +6 -4
- package/lib/feeds/atom.js +1 -1
- package/lib/feeds/capabilities.js +5 -0
- package/lib/feeds/fetcher.js +5 -28
- package/lib/feeds/hfeed.js +1 -1
- package/lib/feeds/jsonfeed.js +1 -1
- package/lib/feeds/normalizer-hfeed.js +209 -0
- package/lib/feeds/normalizer-jsonfeed.js +171 -0
- package/lib/feeds/normalizer-rss.js +178 -0
- package/lib/feeds/normalizer.js +22 -614
- package/lib/feeds/rss.js +1 -1
- package/lib/media/proxy.js +82 -27
- package/lib/polling/processor.js +30 -21
- package/lib/polling/scheduler.js +2 -0
- package/lib/realtime/broker.js +6 -1
- package/lib/storage/channels.js +53 -42
- package/lib/storage/feeds.js +3 -1
- package/lib/storage/items-read-state.js +287 -0
- package/lib/storage/items-retention.js +174 -0
- package/lib/storage/items-search.js +34 -0
- package/lib/storage/items.js +113 -610
- package/lib/storage/read-state.js +1 -1
- package/lib/utils/async-handler.js +7 -0
- package/lib/utils/constants.js +7 -0
- package/lib/utils/csrf.js +51 -0
- package/lib/utils/html.js +25 -0
- package/lib/utils/sanitize.js +61 -0
- package/lib/utils/source-type.js +28 -0
- package/lib/utils/validation.js +8 -2
- package/lib/webmention/processor.js +1 -1
- package/lib/webmention/verifier.js +10 -21
- package/lib/websub/subscriber.js +12 -0
- package/locales/de.json +3 -0
- package/locales/en.json +2 -0
- package/locales/es-419.json +3 -0
- package/locales/es.json +3 -0
- package/locales/fr.json +3 -0
- package/locales/hi.json +3 -0
- package/locales/id.json +3 -0
- package/locales/it.json +3 -0
- package/locales/nl.json +3 -0
- package/locales/pl.json +3 -0
- package/locales/pt-BR.json +3 -0
- package/locales/pt.json +3 -0
- package/locales/sr.json +3 -0
- package/locales/sv.json +3 -0
- package/locales/zh-Hans-CN.json +3 -0
- package/package.json +3 -1
- package/views/actor.njk +2 -0
- package/views/channel-new.njk +1 -0
- package/views/channel.njk +3 -344
- package/views/compose.njk +1 -0
- package/views/deck-settings.njk +1 -0
- package/views/feed-edit.njk +3 -0
- package/views/feeds.njk +4 -0
- package/views/layouts/reader.njk +1 -0
- package/views/search.njk +2 -0
- package/views/settings.njk +2 -0
- package/views/timeline.njk +3 -271
- package/lib/controllers/reader.js +0 -1580
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wrap async route handlers to catch rejections and pass to Express error middleware
|
|
3
|
+
* @param {Function} fn - Async route handler
|
|
4
|
+
* @returns {Function} Wrapped handler
|
|
5
|
+
*/
|
|
6
|
+
export const asyncHandler = (fn) => (req, res, next) =>
|
|
7
|
+
Promise.resolve(fn(req, res, next)).catch(next);
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CSRF protection middleware for reader UI
|
|
3
|
+
* Uses session-based tokens (not cookies)
|
|
4
|
+
* @module utils/csrf
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import crypto from "node:crypto";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Generate or retrieve CSRF token from session.
|
|
11
|
+
* Exposes token as `response.locals.csrfToken` for templates.
|
|
12
|
+
*
|
|
13
|
+
* @param {object} request - Express request
|
|
14
|
+
* @param {object} response - Express response
|
|
15
|
+
* @param {Function} next - Express next
|
|
16
|
+
*/
|
|
17
|
+
export function csrfToken(request, response, next) {
|
|
18
|
+
if (request.session) {
|
|
19
|
+
if (!request.session.csrfToken) {
|
|
20
|
+
request.session.csrfToken = crypto.randomUUID();
|
|
21
|
+
}
|
|
22
|
+
response.locals.csrfToken = request.session.csrfToken;
|
|
23
|
+
}
|
|
24
|
+
next();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Validate CSRF token on POST requests.
|
|
29
|
+
* Checks `_csrf` field in body or `x-csrf-token` header.
|
|
30
|
+
*
|
|
31
|
+
* @param {object} request - Express request
|
|
32
|
+
* @param {object} response - Express response
|
|
33
|
+
* @param {Function} next - Express next
|
|
34
|
+
*/
|
|
35
|
+
export function csrfValidate(request, response, next) {
|
|
36
|
+
if (request.method !== "POST") return next();
|
|
37
|
+
|
|
38
|
+
const sessionToken = request.session?.csrfToken;
|
|
39
|
+
if (!sessionToken) {
|
|
40
|
+
return response.status(403).send("CSRF token missing from session");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const submittedToken =
|
|
44
|
+
request.body?._csrf || request.headers["x-csrf-token"];
|
|
45
|
+
|
|
46
|
+
if (!submittedToken || submittedToken !== sessionToken) {
|
|
47
|
+
return response.status(403).send("CSRF token invalid");
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
next();
|
|
51
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared HTML utilities
|
|
3
|
+
* @module utils/html
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Extract image URLs from HTML content (fallback for items without explicit photos)
|
|
8
|
+
* @param {string} html - HTML content
|
|
9
|
+
* @returns {string[]} Array of image URLs
|
|
10
|
+
*/
|
|
11
|
+
export function extractImagesFromHtml(html) {
|
|
12
|
+
if (!html) {
|
|
13
|
+
return [];
|
|
14
|
+
}
|
|
15
|
+
const urls = [];
|
|
16
|
+
const imgRegex = /<img[^>]+src=["']([^"']+)["'][^>]*>/gi;
|
|
17
|
+
let match;
|
|
18
|
+
while ((match = imgRegex.exec(html)) !== null) {
|
|
19
|
+
const src = match[1];
|
|
20
|
+
if (src && !urls.includes(src)) {
|
|
21
|
+
urls.push(src);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return urls;
|
|
25
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared HTML sanitization configuration
|
|
3
|
+
* Used by both RSS/Atom normalizer and ActivityPub outbox fetcher
|
|
4
|
+
* @module utils/sanitize
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Allowed HTML tags and attributes for sanitize-html
|
|
9
|
+
*/
|
|
10
|
+
export const SANITIZE_OPTIONS = {
|
|
11
|
+
allowedTags: [
|
|
12
|
+
"a",
|
|
13
|
+
"abbr",
|
|
14
|
+
"b",
|
|
15
|
+
"blockquote",
|
|
16
|
+
"br",
|
|
17
|
+
"code",
|
|
18
|
+
"em",
|
|
19
|
+
"figcaption",
|
|
20
|
+
"figure",
|
|
21
|
+
"h1",
|
|
22
|
+
"h2",
|
|
23
|
+
"h3",
|
|
24
|
+
"h4",
|
|
25
|
+
"h5",
|
|
26
|
+
"h6",
|
|
27
|
+
"hr",
|
|
28
|
+
"i",
|
|
29
|
+
"img",
|
|
30
|
+
"li",
|
|
31
|
+
"ol",
|
|
32
|
+
"p",
|
|
33
|
+
"pre",
|
|
34
|
+
"s",
|
|
35
|
+
"span",
|
|
36
|
+
"strike",
|
|
37
|
+
"strong",
|
|
38
|
+
"sub",
|
|
39
|
+
"sup",
|
|
40
|
+
"table",
|
|
41
|
+
"tbody",
|
|
42
|
+
"td",
|
|
43
|
+
"th",
|
|
44
|
+
"thead",
|
|
45
|
+
"tr",
|
|
46
|
+
"u",
|
|
47
|
+
"ul",
|
|
48
|
+
"video",
|
|
49
|
+
"audio",
|
|
50
|
+
"source",
|
|
51
|
+
],
|
|
52
|
+
allowedAttributes: {
|
|
53
|
+
a: ["href", "title", "rel"],
|
|
54
|
+
img: ["src", "alt", "title", "width", "height"],
|
|
55
|
+
video: ["src", "poster", "controls", "width", "height"],
|
|
56
|
+
audio: ["src", "controls"],
|
|
57
|
+
source: ["src", "type"],
|
|
58
|
+
"*": ["class"],
|
|
59
|
+
},
|
|
60
|
+
allowedSchemes: ["http", "https", "mailto"],
|
|
61
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* URL classification utilities
|
|
3
|
+
* @module utils/source-type
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Classify a URL by its platform/protocol
|
|
8
|
+
* @param {string} url - URL to classify
|
|
9
|
+
* @returns {{ type: string, protocol: string }}
|
|
10
|
+
*/
|
|
11
|
+
export function classifyUrl(url) {
|
|
12
|
+
if (!url || typeof url !== "string") return { type: "web", protocol: "web" };
|
|
13
|
+
const lower = url.toLowerCase();
|
|
14
|
+
if (lower.includes("bsky.app") || lower.includes("bluesky")) {
|
|
15
|
+
return { type: "bluesky", protocol: "atmosphere" };
|
|
16
|
+
}
|
|
17
|
+
if (
|
|
18
|
+
lower.includes("mastodon.") ||
|
|
19
|
+
lower.includes("mstdn.") ||
|
|
20
|
+
lower.includes("fosstodon.") ||
|
|
21
|
+
lower.includes("pleroma.") ||
|
|
22
|
+
lower.includes("misskey.") ||
|
|
23
|
+
lower.includes("pixelfed.")
|
|
24
|
+
) {
|
|
25
|
+
return { type: "mastodon", protocol: "fediverse" };
|
|
26
|
+
}
|
|
27
|
+
return { type: "web", protocol: "web" };
|
|
28
|
+
}
|
package/lib/utils/validation.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { IndiekitError } from "@indiekit/error";
|
|
7
|
+
import safeRegex from "safe-regex2";
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* Valid Microsub actions
|
|
@@ -30,7 +31,7 @@ export const VALID_CHANNEL_METHODS = ["delete", "order"];
|
|
|
30
31
|
/**
|
|
31
32
|
* Valid timeline methods
|
|
32
33
|
*/
|
|
33
|
-
export const VALID_TIMELINE_METHODS = ["mark_read", "mark_unread", "remove"];
|
|
34
|
+
export const VALID_TIMELINE_METHODS = ["mark_read", "mark_read_source", "mark_unread", "remove"];
|
|
34
35
|
|
|
35
36
|
/**
|
|
36
37
|
* Valid exclude types for channel filtering
|
|
@@ -173,7 +174,12 @@ export function validateExcludeRegex(pattern) {
|
|
|
173
174
|
}
|
|
174
175
|
|
|
175
176
|
try {
|
|
176
|
-
new RegExp(pattern);
|
|
177
|
+
const regex = new RegExp(pattern);
|
|
178
|
+
// Reject patterns vulnerable to catastrophic backtracking (ReDoS)
|
|
179
|
+
if (!safeRegex(regex)) {
|
|
180
|
+
console.warn(`[Microsub] Rejected unsafe regex pattern: ${pattern}`);
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
177
183
|
return pattern;
|
|
178
184
|
} catch {
|
|
179
185
|
return;
|
|
@@ -78,7 +78,7 @@ export async function processWebmention(application, source, target, userId) {
|
|
|
78
78
|
}
|
|
79
79
|
|
|
80
80
|
// Publish real-time event
|
|
81
|
-
const redis = getRedisClient(application);
|
|
81
|
+
const redis = await getRedisClient(application);
|
|
82
82
|
if (redis && userId) {
|
|
83
83
|
await publishEvent(redis, `microsub:user:${userId}`, {
|
|
84
84
|
type: "new-notification",
|
|
@@ -6,27 +6,8 @@
|
|
|
6
6
|
import { mf2 } from "microformats-parser";
|
|
7
7
|
import sanitizeHtml from "sanitize-html";
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
*/
|
|
12
|
-
const SANITIZE_OPTIONS = {
|
|
13
|
-
allowedTags: [
|
|
14
|
-
"a", "abbr", "b", "blockquote", "br", "code", "em", "figcaption",
|
|
15
|
-
"figure", "h1", "h2", "h3", "h4", "h5", "h6", "hr", "i", "img",
|
|
16
|
-
"li", "ol", "p", "pre", "s", "span", "strike", "strong", "sub",
|
|
17
|
-
"sup", "table", "tbody", "td", "th", "thead", "tr", "u", "ul",
|
|
18
|
-
"video", "audio", "source",
|
|
19
|
-
],
|
|
20
|
-
allowedAttributes: {
|
|
21
|
-
a: ["href", "title", "rel"],
|
|
22
|
-
img: ["src", "alt", "title", "width", "height"],
|
|
23
|
-
video: ["src", "poster", "controls", "width", "height"],
|
|
24
|
-
audio: ["src", "controls"],
|
|
25
|
-
source: ["src", "type"],
|
|
26
|
-
"*": ["class"],
|
|
27
|
-
},
|
|
28
|
-
allowedSchemes: ["http", "https", "mailto"],
|
|
29
|
-
};
|
|
9
|
+
import { isPrivateUrl } from "../media/proxy.js";
|
|
10
|
+
import { SANITIZE_OPTIONS } from "../utils/sanitize.js";
|
|
30
11
|
|
|
31
12
|
/**
|
|
32
13
|
* Verify a webmention
|
|
@@ -36,6 +17,14 @@ const SANITIZE_OPTIONS = {
|
|
|
36
17
|
*/
|
|
37
18
|
export async function verifyWebmention(source, target) {
|
|
38
19
|
try {
|
|
20
|
+
// SSRF protection — block private/internal IPs (highest risk: unauthenticated endpoint)
|
|
21
|
+
if (await isPrivateUrl(source)) {
|
|
22
|
+
return {
|
|
23
|
+
verified: false,
|
|
24
|
+
error: "Source URL blocked (private/internal address)",
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
39
28
|
// Fetch the source URL
|
|
40
29
|
const response = await fetch(source, {
|
|
41
30
|
headers: {
|
package/lib/websub/subscriber.js
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
import crypto from "node:crypto";
|
|
7
7
|
|
|
8
|
+
import { isPrivateUrl } from "../media/proxy.js";
|
|
8
9
|
import { updateFeedWebsub } from "../storage/feeds.js";
|
|
9
10
|
|
|
10
11
|
const DEFAULT_LEASE_SECONDS = 86_400 * 7; // 7 days
|
|
@@ -24,6 +25,12 @@ export async function subscribe(application, feed, callbackUrl) {
|
|
|
24
25
|
const topic = feed.websub.topic || feed.url;
|
|
25
26
|
const secret = generateSecret();
|
|
26
27
|
|
|
28
|
+
// SSRF protection — hub URL comes from untrusted feed content
|
|
29
|
+
if (await isPrivateUrl(feed.websub.hub)) {
|
|
30
|
+
console.warn(`[Microsub] WebSub blocked private hub URL: ${feed.websub.hub}`);
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
|
|
27
34
|
try {
|
|
28
35
|
const response = await fetch(feed.websub.hub, {
|
|
29
36
|
method: "POST",
|
|
@@ -137,6 +144,11 @@ export function verifySignature(signature, body, secret) {
|
|
|
137
144
|
// Normalize algorithm name
|
|
138
145
|
const algo = algorithm.toLowerCase().replace("sha", "sha");
|
|
139
146
|
|
|
147
|
+
// Warn about deprecated SHA-1 (accepted for compatibility, but SHA-256 preferred)
|
|
148
|
+
if (algo === "sha1") {
|
|
149
|
+
console.warn("[Microsub] WebSub: hub using deprecated SHA-1 signature (SHA-256 preferred)");
|
|
150
|
+
}
|
|
151
|
+
|
|
140
152
|
try {
|
|
141
153
|
const expectedHash = crypto
|
|
142
154
|
.createHmac(algo, secret)
|
package/locales/de.json
CHANGED
|
@@ -4,6 +4,9 @@
|
|
|
4
4
|
"title": "Leser",
|
|
5
5
|
"empty": "Keine Elemente zum Anzeigen",
|
|
6
6
|
"markAllRead": "Alle als gelesen markieren",
|
|
7
|
+
"markViewRead": "Mark current view as read",
|
|
8
|
+
"filterChannels": "Filter channels",
|
|
9
|
+
"apply": "Apply",
|
|
7
10
|
"showRead": "Gelesene anzeigen ({{count}})",
|
|
8
11
|
"hideRead": "Gelesene Elemente ausblenden",
|
|
9
12
|
"allRead": "Alles aufgeholt!",
|
package/locales/en.json
CHANGED
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
"empty": "No items to display",
|
|
6
6
|
"markAllRead": "Mark all as read",
|
|
7
7
|
"markViewRead": "Mark current view as read",
|
|
8
|
+
"filterChannels": "Filter channels",
|
|
9
|
+
"apply": "Apply",
|
|
8
10
|
"showRead": "Show read ({{count}})",
|
|
9
11
|
"hideRead": "Hide read items",
|
|
10
12
|
"allRead": "All caught up!",
|
package/locales/es-419.json
CHANGED
|
@@ -4,6 +4,9 @@
|
|
|
4
4
|
"title": "Lector",
|
|
5
5
|
"empty": "No hay elementos para mostrar",
|
|
6
6
|
"markAllRead": "Marcar todo como leído",
|
|
7
|
+
"markViewRead": "Mark current view as read",
|
|
8
|
+
"filterChannels": "Filter channels",
|
|
9
|
+
"apply": "Apply",
|
|
7
10
|
"showRead": "Mostrar leídos ({{count}})",
|
|
8
11
|
"hideRead": "Ocultar elementos leídos",
|
|
9
12
|
"allRead": "¡Todo al día!",
|
package/locales/es.json
CHANGED
|
@@ -4,6 +4,9 @@
|
|
|
4
4
|
"title": "Lector",
|
|
5
5
|
"empty": "No hay elementos para mostrar",
|
|
6
6
|
"markAllRead": "Marcar todo como leído",
|
|
7
|
+
"markViewRead": "Mark current view as read",
|
|
8
|
+
"filterChannels": "Filter channels",
|
|
9
|
+
"apply": "Apply",
|
|
7
10
|
"showRead": "Mostrar leídos ({{count}})",
|
|
8
11
|
"hideRead": "Ocultar elementos leídos",
|
|
9
12
|
"allRead": "¡Todo al día!",
|
package/locales/fr.json
CHANGED
|
@@ -4,6 +4,9 @@
|
|
|
4
4
|
"title": "Lecteur",
|
|
5
5
|
"empty": "Aucun élément à afficher",
|
|
6
6
|
"markAllRead": "Tout marquer comme lu",
|
|
7
|
+
"markViewRead": "Mark current view as read",
|
|
8
|
+
"filterChannels": "Filter channels",
|
|
9
|
+
"apply": "Apply",
|
|
7
10
|
"showRead": "Afficher les lus ({{count}})",
|
|
8
11
|
"hideRead": "Masquer les éléments lus",
|
|
9
12
|
"allRead": "Tout est à jour !",
|
package/locales/hi.json
CHANGED
|
@@ -4,6 +4,9 @@
|
|
|
4
4
|
"title": "रीडर",
|
|
5
5
|
"empty": "प्रदर्शित करने के लिए कोई आइटम नहीं",
|
|
6
6
|
"markAllRead": "सभी को पढ़ा हुआ चिह्नित करें",
|
|
7
|
+
"markViewRead": "Mark current view as read",
|
|
8
|
+
"filterChannels": "Filter channels",
|
|
9
|
+
"apply": "Apply",
|
|
7
10
|
"showRead": "पढ़े हुए दिखाएं ({{count}})",
|
|
8
11
|
"hideRead": "पढ़े हुए आइटम छिपाएं",
|
|
9
12
|
"allRead": "सब पकड़ लिया!",
|
package/locales/id.json
CHANGED
|
@@ -4,6 +4,9 @@
|
|
|
4
4
|
"title": "Pembaca",
|
|
5
5
|
"empty": "Tidak ada item untuk ditampilkan",
|
|
6
6
|
"markAllRead": "Tandai semua sudah dibaca",
|
|
7
|
+
"markViewRead": "Mark current view as read",
|
|
8
|
+
"filterChannels": "Filter channels",
|
|
9
|
+
"apply": "Apply",
|
|
7
10
|
"showRead": "Tampilkan yang sudah dibaca ({{count}})",
|
|
8
11
|
"hideRead": "Sembunyikan item yang sudah dibaca",
|
|
9
12
|
"allRead": "Semua sudah terbaca!",
|
package/locales/it.json
CHANGED
|
@@ -4,6 +4,9 @@
|
|
|
4
4
|
"title": "Lettore",
|
|
5
5
|
"empty": "Nessun elemento da visualizzare",
|
|
6
6
|
"markAllRead": "Segna tutto come letto",
|
|
7
|
+
"markViewRead": "Mark current view as read",
|
|
8
|
+
"filterChannels": "Filter channels",
|
|
9
|
+
"apply": "Apply",
|
|
7
10
|
"showRead": "Mostra letti ({{count}})",
|
|
8
11
|
"hideRead": "Nascondi elementi letti",
|
|
9
12
|
"allRead": "Tutto aggiornato!",
|
package/locales/nl.json
CHANGED
|
@@ -4,6 +4,9 @@
|
|
|
4
4
|
"title": "Lezer",
|
|
5
5
|
"empty": "Geen items om weer te geven",
|
|
6
6
|
"markAllRead": "Alles als gelezen markeren",
|
|
7
|
+
"markViewRead": "Mark current view as read",
|
|
8
|
+
"filterChannels": "Filter channels",
|
|
9
|
+
"apply": "Apply",
|
|
7
10
|
"showRead": "Toon gelezen ({{count}})",
|
|
8
11
|
"hideRead": "Verberg gelezen items",
|
|
9
12
|
"allRead": "Alles bijgewerkt!",
|
package/locales/pl.json
CHANGED
|
@@ -4,6 +4,9 @@
|
|
|
4
4
|
"title": "Czytnik",
|
|
5
5
|
"empty": "Brak elementów do wyświetlenia",
|
|
6
6
|
"markAllRead": "Oznacz wszystkie jako przeczytane",
|
|
7
|
+
"markViewRead": "Mark current view as read",
|
|
8
|
+
"filterChannels": "Filter channels",
|
|
9
|
+
"apply": "Apply",
|
|
7
10
|
"showRead": "Pokaż przeczytane ({{count}})",
|
|
8
11
|
"hideRead": "Ukryj przeczytane elementy",
|
|
9
12
|
"allRead": "Wszystko przeczytane!",
|
package/locales/pt-BR.json
CHANGED
|
@@ -4,6 +4,9 @@
|
|
|
4
4
|
"title": "Leitor",
|
|
5
5
|
"empty": "Nenhum item para exibir",
|
|
6
6
|
"markAllRead": "Marcar tudo como lido",
|
|
7
|
+
"markViewRead": "Mark current view as read",
|
|
8
|
+
"filterChannels": "Filter channels",
|
|
9
|
+
"apply": "Apply",
|
|
7
10
|
"showRead": "Mostrar lidos ({{count}})",
|
|
8
11
|
"hideRead": "Ocultar itens lidos",
|
|
9
12
|
"allRead": "Tudo em dia!",
|
package/locales/pt.json
CHANGED
|
@@ -4,6 +4,9 @@
|
|
|
4
4
|
"title": "Leitor",
|
|
5
5
|
"empty": "Nenhum item para exibir",
|
|
6
6
|
"markAllRead": "Marcar tudo como lido",
|
|
7
|
+
"markViewRead": "Mark current view as read",
|
|
8
|
+
"filterChannels": "Filter channels",
|
|
9
|
+
"apply": "Apply",
|
|
7
10
|
"showRead": "Mostrar lidos ({{count}})",
|
|
8
11
|
"hideRead": "Ocultar itens lidos",
|
|
9
12
|
"allRead": "Tudo em dia!",
|
package/locales/sr.json
CHANGED
|
@@ -4,6 +4,9 @@
|
|
|
4
4
|
"title": "Читач",
|
|
5
5
|
"empty": "Нема ставки за приказ",
|
|
6
6
|
"markAllRead": "Означи све као прочитано",
|
|
7
|
+
"markViewRead": "Mark current view as read",
|
|
8
|
+
"filterChannels": "Filter channels",
|
|
9
|
+
"apply": "Apply",
|
|
7
10
|
"showRead": "Прикажи прочитано ({{count}})",
|
|
8
11
|
"hideRead": "Сакриј прочитане ставке",
|
|
9
12
|
"allRead": "Све ажурирано!",
|
package/locales/sv.json
CHANGED
|
@@ -4,6 +4,9 @@
|
|
|
4
4
|
"title": "Läsare",
|
|
5
5
|
"empty": "Inga objekt att visa",
|
|
6
6
|
"markAllRead": "Markera allt som läst",
|
|
7
|
+
"markViewRead": "Mark current view as read",
|
|
8
|
+
"filterChannels": "Filter channels",
|
|
9
|
+
"apply": "Apply",
|
|
7
10
|
"showRead": "Visa lästa ({{count}})",
|
|
8
11
|
"hideRead": "Dölj lästa objekt",
|
|
9
12
|
"allRead": "Allt är uppdaterat!",
|
package/locales/zh-Hans-CN.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rmdes/indiekit-endpoint-microsub",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.57",
|
|
4
4
|
"description": "Microsub endpoint for Indiekit. Enables subscribing to feeds and reading content using the Microsub protocol.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"indiekit",
|
|
@@ -46,6 +46,8 @@
|
|
|
46
46
|
"ioredis": "^5.3.0",
|
|
47
47
|
"luxon": "^3.4.0",
|
|
48
48
|
"microformats-parser": "^2.0.0",
|
|
49
|
+
"express-rate-limit": "^7.0.0",
|
|
50
|
+
"safe-regex2": "^4.0.0",
|
|
49
51
|
"sanitize-html": "^2.11.0"
|
|
50
52
|
},
|
|
51
53
|
"publishConfig": {
|
package/views/actor.njk
CHANGED
|
@@ -46,6 +46,7 @@
|
|
|
46
46
|
{% if canFollow %}
|
|
47
47
|
{% if isFollowing %}
|
|
48
48
|
<form action="{{ baseUrl }}/actor/unfollow" method="POST" style="display: inline;">
|
|
49
|
+
<input type="hidden" name="_csrf" value="{{ csrfToken }}">
|
|
49
50
|
<input type="hidden" name="actorUrl" value="{{ actorUrl }}">
|
|
50
51
|
<button type="submit" class="button button--secondary button--small">
|
|
51
52
|
{{ icon("tick") }} Following
|
|
@@ -53,6 +54,7 @@
|
|
|
53
54
|
</form>
|
|
54
55
|
{% else %}
|
|
55
56
|
<form action="{{ baseUrl }}/actor/follow" method="POST" style="display: inline;">
|
|
57
|
+
<input type="hidden" name="_csrf" value="{{ csrfToken }}">
|
|
56
58
|
<input type="hidden" name="actorUrl" value="{{ actorUrl }}">
|
|
57
59
|
<input type="hidden" name="actorName" value="{{ actor.name }}">
|
|
58
60
|
<button type="submit" class="button button--primary button--small">
|