@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
package/lib/cache/redis.js
CHANGED
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
* @module cache/redis
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
// Redis is dynamically imported only when needed so the package is optional
|
|
7
|
+
let Redis;
|
|
8
8
|
let redisClient;
|
|
9
9
|
|
|
10
10
|
/**
|
|
@@ -12,7 +12,7 @@ let redisClient;
|
|
|
12
12
|
* @param {object} application - Indiekit application
|
|
13
13
|
* @returns {object|undefined} Redis client or undefined
|
|
14
14
|
*/
|
|
15
|
-
export function getRedisClient(application) {
|
|
15
|
+
export async function getRedisClient(application) {
|
|
16
16
|
// Check if Redis is already initialized on the application
|
|
17
17
|
if (application.redis) {
|
|
18
18
|
return application.redis;
|
|
@@ -27,6 +27,15 @@ export function getRedisClient(application) {
|
|
|
27
27
|
const redisUrl = application.config?.application?.redisUrl;
|
|
28
28
|
if (redisUrl) {
|
|
29
29
|
try {
|
|
30
|
+
if (!Redis) {
|
|
31
|
+
try {
|
|
32
|
+
Redis = (await import("ioredis")).default;
|
|
33
|
+
} catch {
|
|
34
|
+
console.warn("[Microsub] ioredis not available");
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
30
39
|
redisClient = new Redis(redisUrl, {
|
|
31
40
|
maxRetriesPerRequest: 3,
|
|
32
41
|
retryStrategy(times) {
|
|
@@ -56,6 +65,15 @@ export function getRedisClient(application) {
|
|
|
56
65
|
}
|
|
57
66
|
}
|
|
58
67
|
|
|
68
|
+
/**
|
|
69
|
+
* Namespace cache keys to prevent cross-instance collisions
|
|
70
|
+
* @param {string} key - Raw cache key
|
|
71
|
+
* @returns {string} Namespaced key
|
|
72
|
+
*/
|
|
73
|
+
function nsKey(key) {
|
|
74
|
+
return `microsub:${key}`;
|
|
75
|
+
}
|
|
76
|
+
|
|
59
77
|
/**
|
|
60
78
|
* Get value from cache
|
|
61
79
|
* @param {object} redis - Redis client
|
|
@@ -68,7 +86,7 @@ export async function getCache(redis, key) {
|
|
|
68
86
|
}
|
|
69
87
|
|
|
70
88
|
try {
|
|
71
|
-
const value = await redis.get(key);
|
|
89
|
+
const value = await redis.get(nsKey(key));
|
|
72
90
|
if (value) {
|
|
73
91
|
return JSON.parse(value);
|
|
74
92
|
}
|
|
@@ -92,9 +110,10 @@ export async function setCache(redis, key, value, ttl = 300) {
|
|
|
92
110
|
|
|
93
111
|
try {
|
|
94
112
|
const serialized = JSON.stringify(value);
|
|
113
|
+
const nk = nsKey(key);
|
|
95
114
|
await (ttl
|
|
96
|
-
? redis.set(
|
|
97
|
-
: redis.set(
|
|
115
|
+
? redis.set(nk, serialized, "EX", ttl)
|
|
116
|
+
: redis.set(nk, serialized));
|
|
98
117
|
} catch {
|
|
99
118
|
// Ignore cache errors
|
|
100
119
|
}
|
|
@@ -112,7 +131,7 @@ export async function deleteCache(redis, key) {
|
|
|
112
131
|
}
|
|
113
132
|
|
|
114
133
|
try {
|
|
115
|
-
await redis.del(key);
|
|
134
|
+
await redis.del(nsKey(key));
|
|
116
135
|
} catch {
|
|
117
136
|
// Ignore cache errors
|
|
118
137
|
}
|
|
@@ -17,7 +17,7 @@ import { getUserId } from "../utils/auth.js";
|
|
|
17
17
|
import {
|
|
18
18
|
validateChannel,
|
|
19
19
|
validateChannelName,
|
|
20
|
-
parseArrayParameter
|
|
20
|
+
parseArrayParameter,
|
|
21
21
|
} from "../utils/validation.js";
|
|
22
22
|
|
|
23
23
|
/**
|
|
@@ -62,7 +62,7 @@ export async function action(request, response) {
|
|
|
62
62
|
|
|
63
63
|
// Reorder channels
|
|
64
64
|
if (method === "order") {
|
|
65
|
-
const channelUids =
|
|
65
|
+
const channelUids = parseArrayParameter(request.body, "channels");
|
|
66
66
|
if (channelUids.length === 0) {
|
|
67
67
|
throw new IndiekitError("Missing channels[] parameter", {
|
|
68
68
|
status: 400,
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ActivityPub actor profiles
|
|
3
|
+
* @module controllers/reader/actor
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { fetchActorOutbox } from "../../activitypub/outbox-fetcher.js";
|
|
7
|
+
|
|
8
|
+
const ACTOR_OUTBOX_LIMIT = 30;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Find the ActivityPub plugin instance from installed plugins.
|
|
12
|
+
* @param {object} request - Express request
|
|
13
|
+
* @returns {object|undefined} The AP plugin instance
|
|
14
|
+
*/
|
|
15
|
+
function getApPlugin(request) {
|
|
16
|
+
const installedPlugins = request.app.locals.installedPlugins;
|
|
17
|
+
if (!installedPlugins) return undefined;
|
|
18
|
+
return [...installedPlugins].find(
|
|
19
|
+
(p) => p.name === "ActivityPub endpoint",
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Actor profile — fetch and display a remote AP actor's recent posts
|
|
25
|
+
* @param {object} request - Express request
|
|
26
|
+
* @param {object} response - Express response
|
|
27
|
+
*/
|
|
28
|
+
export async function actorProfile(request, response) {
|
|
29
|
+
const actorUrl = request.query.url;
|
|
30
|
+
if (!actorUrl) {
|
|
31
|
+
return response.status(400).render("404");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Check if we already follow this actor
|
|
35
|
+
const { application } = request.app.locals;
|
|
36
|
+
const apFollowing = application?.collections?.get("ap_following");
|
|
37
|
+
let isFollowing = false;
|
|
38
|
+
if (apFollowing) {
|
|
39
|
+
const existing = await apFollowing.findOne({ actorUrl });
|
|
40
|
+
isFollowing = !!existing;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Check if AP plugin is available (for follow button visibility)
|
|
44
|
+
const apPlugin = getApPlugin(request);
|
|
45
|
+
const canFollow = !!apPlugin;
|
|
46
|
+
|
|
47
|
+
try {
|
|
48
|
+
const { actor, items } = await fetchActorOutbox(actorUrl, { limit: ACTOR_OUTBOX_LIMIT });
|
|
49
|
+
|
|
50
|
+
response.render("actor", {
|
|
51
|
+
title: actor.name || "Actor",
|
|
52
|
+
actor,
|
|
53
|
+
items,
|
|
54
|
+
actorUrl,
|
|
55
|
+
isFollowing,
|
|
56
|
+
canFollow,
|
|
57
|
+
baseUrl: request.baseUrl,
|
|
58
|
+
readerBaseUrl: request.baseUrl,
|
|
59
|
+
activeView: "channels",
|
|
60
|
+
breadcrumbs: [
|
|
61
|
+
{ text: "Reader", href: request.baseUrl },
|
|
62
|
+
{ text: actor.name || "Actor" },
|
|
63
|
+
],
|
|
64
|
+
});
|
|
65
|
+
} catch (error) {
|
|
66
|
+
console.error(`[Microsub] Actor profile fetch failed: ${error.message}`);
|
|
67
|
+
response.render("actor", {
|
|
68
|
+
title: "Actor",
|
|
69
|
+
actor: { name: actorUrl, url: actorUrl, photo: "", summary: "" },
|
|
70
|
+
items: [],
|
|
71
|
+
actorUrl,
|
|
72
|
+
isFollowing,
|
|
73
|
+
canFollow,
|
|
74
|
+
baseUrl: request.baseUrl,
|
|
75
|
+
readerBaseUrl: request.baseUrl,
|
|
76
|
+
activeView: "channels",
|
|
77
|
+
error: "Could not fetch this actor's profile. They may have restricted access.",
|
|
78
|
+
breadcrumbs: [
|
|
79
|
+
{ text: "Reader", href: request.baseUrl },
|
|
80
|
+
{ text: "Actor" },
|
|
81
|
+
],
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Follow an ActivityPub actor
|
|
88
|
+
* @param {object} request - Express request
|
|
89
|
+
* @param {object} response - Express response
|
|
90
|
+
*/
|
|
91
|
+
export async function followActorAction(request, response) {
|
|
92
|
+
const { actorUrl, actorName } = request.body;
|
|
93
|
+
if (!actorUrl) {
|
|
94
|
+
return response.status(400).redirect(request.baseUrl + "/channels/activitypub");
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const apPlugin = getApPlugin(request);
|
|
98
|
+
if (!apPlugin) {
|
|
99
|
+
console.error("[Microsub] Cannot follow: ActivityPub plugin not installed");
|
|
100
|
+
return response.redirect(
|
|
101
|
+
`${request.baseUrl}/actor?url=${encodeURIComponent(actorUrl)}`,
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const result = await apPlugin.followActor(actorUrl, { name: actorName });
|
|
106
|
+
if (!result.ok) {
|
|
107
|
+
console.error(`[Microsub] Follow via AP plugin failed: ${result.error}`);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return response.redirect(
|
|
111
|
+
`${request.baseUrl}/actor?url=${encodeURIComponent(actorUrl)}`,
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Unfollow an ActivityPub actor
|
|
117
|
+
* @param {object} request - Express request
|
|
118
|
+
* @param {object} response - Express response
|
|
119
|
+
*/
|
|
120
|
+
export async function unfollowActorAction(request, response) {
|
|
121
|
+
const { actorUrl } = request.body;
|
|
122
|
+
if (!actorUrl) {
|
|
123
|
+
return response.status(400).redirect(request.baseUrl + "/channels/activitypub");
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const apPlugin = getApPlugin(request);
|
|
127
|
+
if (!apPlugin) {
|
|
128
|
+
console.error("[Microsub] Cannot unfollow: ActivityPub plugin not installed");
|
|
129
|
+
return response.redirect(
|
|
130
|
+
`${request.baseUrl}/actor?url=${encodeURIComponent(actorUrl)}`,
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const result = await apPlugin.unfollowActor(actorUrl);
|
|
135
|
+
if (!result.ok) {
|
|
136
|
+
console.error(`[Microsub] Unfollow via AP plugin failed: ${result.error}`);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return response.redirect(
|
|
140
|
+
`${request.baseUrl}/actor?url=${encodeURIComponent(actorUrl)}`,
|
|
141
|
+
);
|
|
142
|
+
}
|
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Channel CRUD + HTML fragments
|
|
3
|
+
* @module controllers/reader/channel
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
getChannels,
|
|
8
|
+
getChannel,
|
|
9
|
+
createChannel,
|
|
10
|
+
updateChannelSettings,
|
|
11
|
+
deleteChannel,
|
|
12
|
+
} from "../../storage/channels.js";
|
|
13
|
+
import { getFeedsForChannel } from "../../storage/feeds.js";
|
|
14
|
+
import { getTimelineItems } from "../../storage/items.js";
|
|
15
|
+
import { countReadItems } from "../../storage/items-read-state.js";
|
|
16
|
+
import { getUserId } from "../../utils/auth.js";
|
|
17
|
+
import {
|
|
18
|
+
validateChannelName,
|
|
19
|
+
validateExcludeTypes,
|
|
20
|
+
validateExcludeRegex,
|
|
21
|
+
} from "../../utils/validation.js";
|
|
22
|
+
import { proxyItemImages } from "../../media/proxy.js";
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Reader index - redirect to channels
|
|
26
|
+
* @param {object} request - Express request
|
|
27
|
+
* @param {object} response - Express response
|
|
28
|
+
*/
|
|
29
|
+
export async function index(request, response) {
|
|
30
|
+
const lastView = request.session?.microsubView || "timeline";
|
|
31
|
+
const validViews = ["channels", "deck", "timeline"];
|
|
32
|
+
const view = validViews.includes(lastView) ? lastView : "timeline";
|
|
33
|
+
response.redirect(`${request.baseUrl}/${view}`);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* List channels
|
|
38
|
+
* @param {object} request - Express request
|
|
39
|
+
* @param {object} response - Express response
|
|
40
|
+
*/
|
|
41
|
+
export async function channels(request, response) {
|
|
42
|
+
const { application } = request.app.locals;
|
|
43
|
+
const userId = getUserId(request);
|
|
44
|
+
|
|
45
|
+
const channelList = await getChannels(application, userId);
|
|
46
|
+
|
|
47
|
+
if (request.session) request.session.microsubView = "channels";
|
|
48
|
+
|
|
49
|
+
response.render("reader", {
|
|
50
|
+
title: request.__("microsub.views.channels"),
|
|
51
|
+
channels: channelList,
|
|
52
|
+
baseUrl: request.baseUrl,
|
|
53
|
+
readerBaseUrl: request.baseUrl,
|
|
54
|
+
activeView: "channels",
|
|
55
|
+
breadcrumbs: [
|
|
56
|
+
{ text: "Reader", href: request.baseUrl },
|
|
57
|
+
{ text: "Channels" },
|
|
58
|
+
],
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* New channel form
|
|
64
|
+
* @param {object} request - Express request
|
|
65
|
+
* @param {object} response - Express response
|
|
66
|
+
*/
|
|
67
|
+
export async function newChannel(request, response) {
|
|
68
|
+
response.render("channel-new", {
|
|
69
|
+
title: request.__("microsub.channels.new"),
|
|
70
|
+
baseUrl: request.baseUrl,
|
|
71
|
+
readerBaseUrl: request.baseUrl,
|
|
72
|
+
activeView: "channels",
|
|
73
|
+
breadcrumbs: [
|
|
74
|
+
{ text: "Reader", href: request.baseUrl },
|
|
75
|
+
{ text: "Channels", href: `${request.baseUrl}/channels` },
|
|
76
|
+
{ text: request.__("microsub.channels.new") },
|
|
77
|
+
],
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Create channel
|
|
83
|
+
* @param {object} request - Express request
|
|
84
|
+
* @param {object} response - Express response
|
|
85
|
+
*/
|
|
86
|
+
export async function createChannelAction(request, response) {
|
|
87
|
+
const { application } = request.app.locals;
|
|
88
|
+
const userId = getUserId(request);
|
|
89
|
+
const { name } = request.body;
|
|
90
|
+
|
|
91
|
+
validateChannelName(name);
|
|
92
|
+
|
|
93
|
+
await createChannel(application, { name, userId });
|
|
94
|
+
|
|
95
|
+
response.redirect(`${request.baseUrl}/channels`);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* View channel timeline
|
|
100
|
+
* @param {object} request - Express request
|
|
101
|
+
* @param {object} response - Express response
|
|
102
|
+
* @returns {Promise<void>}
|
|
103
|
+
*/
|
|
104
|
+
export async function channel(request, response) {
|
|
105
|
+
const { application } = request.app.locals;
|
|
106
|
+
const userId = getUserId(request);
|
|
107
|
+
const { uid } = request.params;
|
|
108
|
+
const { before, after, showRead } = request.query;
|
|
109
|
+
|
|
110
|
+
const channelDocument = await getChannel(application, uid, userId);
|
|
111
|
+
if (!channelDocument) {
|
|
112
|
+
return response.status(404).render("404");
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Check if showing read items
|
|
116
|
+
const showReadItems = showRead === "true";
|
|
117
|
+
|
|
118
|
+
const timeline = await getTimelineItems(application, channelDocument._id, {
|
|
119
|
+
before,
|
|
120
|
+
after,
|
|
121
|
+
userId,
|
|
122
|
+
showRead: showReadItems,
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
// Proxy images through media endpoint for privacy
|
|
126
|
+
const proxyBaseUrl = application.url;
|
|
127
|
+
if (proxyBaseUrl && timeline.items) {
|
|
128
|
+
timeline.items = timeline.items.map((item) =>
|
|
129
|
+
proxyItemImages(item, proxyBaseUrl),
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Count read items to show "View read items" button
|
|
134
|
+
const readCount = await countReadItems(
|
|
135
|
+
application,
|
|
136
|
+
channelDocument._id,
|
|
137
|
+
userId,
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
response.render("channel", {
|
|
141
|
+
title: channelDocument.name,
|
|
142
|
+
channel: channelDocument,
|
|
143
|
+
items: timeline.items,
|
|
144
|
+
paging: timeline.paging,
|
|
145
|
+
readCount,
|
|
146
|
+
showRead: showReadItems,
|
|
147
|
+
baseUrl: request.baseUrl,
|
|
148
|
+
readerBaseUrl: request.baseUrl,
|
|
149
|
+
activeView: "channels",
|
|
150
|
+
breadcrumbs: [
|
|
151
|
+
{ text: "Reader", href: request.baseUrl },
|
|
152
|
+
{ text: "Channels", href: `${request.baseUrl}/channels` },
|
|
153
|
+
{ text: channelDocument.name },
|
|
154
|
+
],
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Return rendered HTML fragments for infinite scroll
|
|
160
|
+
* @param {object} request - Express request
|
|
161
|
+
* @param {object} response - Express response
|
|
162
|
+
* @returns {Promise<void>}
|
|
163
|
+
*/
|
|
164
|
+
export async function channelHtml(request, response) {
|
|
165
|
+
const { application } = request.app.locals;
|
|
166
|
+
const userId = getUserId(request);
|
|
167
|
+
const { uid } = request.params;
|
|
168
|
+
const { before, after, showRead } = request.query;
|
|
169
|
+
|
|
170
|
+
const channelDocument = await getChannel(application, uid, userId);
|
|
171
|
+
if (!channelDocument) {
|
|
172
|
+
return response.status(404).json({ error: "Channel not found" });
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const showReadItems = showRead === "true";
|
|
176
|
+
|
|
177
|
+
const timeline = await getTimelineItems(application, channelDocument._id, {
|
|
178
|
+
before,
|
|
179
|
+
after,
|
|
180
|
+
userId,
|
|
181
|
+
showRead: showReadItems,
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
// Proxy images
|
|
185
|
+
const proxyBaseUrl = application.url;
|
|
186
|
+
if (proxyBaseUrl && timeline.items) {
|
|
187
|
+
timeline.items = timeline.items.map((item) =>
|
|
188
|
+
proxyItemImages(item, proxyBaseUrl),
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Render items via layout-less fragment template (standard response.render
|
|
193
|
+
// with callback returns HTML string without sending a response)
|
|
194
|
+
const fragmentHtml = await new Promise((resolve, reject) => {
|
|
195
|
+
response.render("partials/items-fragment", {
|
|
196
|
+
items: timeline.items,
|
|
197
|
+
channel: channelDocument,
|
|
198
|
+
baseUrl: request.baseUrl,
|
|
199
|
+
}, (error, html) => error ? reject(error) : resolve(html));
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
response.json({
|
|
203
|
+
html: fragmentHtml,
|
|
204
|
+
paging: timeline.paging,
|
|
205
|
+
count: timeline.items.length,
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Channel settings form
|
|
211
|
+
* @param {object} request - Express request
|
|
212
|
+
* @param {object} response - Express response
|
|
213
|
+
* @returns {Promise<void>}
|
|
214
|
+
*/
|
|
215
|
+
export async function settings(request, response) {
|
|
216
|
+
const { application } = request.app.locals;
|
|
217
|
+
const userId = getUserId(request);
|
|
218
|
+
const { uid } = request.params;
|
|
219
|
+
|
|
220
|
+
const channelDocument = await getChannel(application, uid, userId);
|
|
221
|
+
if (!channelDocument) {
|
|
222
|
+
return response.status(404).render("404");
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
response.render("settings", {
|
|
226
|
+
title: request.__("microsub.settings.title", {
|
|
227
|
+
channel: channelDocument.name,
|
|
228
|
+
}),
|
|
229
|
+
channel: channelDocument,
|
|
230
|
+
baseUrl: request.baseUrl,
|
|
231
|
+
readerBaseUrl: request.baseUrl,
|
|
232
|
+
activeView: "channels",
|
|
233
|
+
breadcrumbs: [
|
|
234
|
+
{ text: "Reader", href: request.baseUrl },
|
|
235
|
+
{ text: "Channels", href: `${request.baseUrl}/channels` },
|
|
236
|
+
{ text: channelDocument.name, href: `${request.baseUrl}/channels/${uid}` },
|
|
237
|
+
{ text: "Settings" },
|
|
238
|
+
],
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Update channel settings
|
|
244
|
+
* @param {object} request - Express request
|
|
245
|
+
* @param {object} response - Express response
|
|
246
|
+
* @returns {Promise<void>}
|
|
247
|
+
*/
|
|
248
|
+
export async function updateSettings(request, response) {
|
|
249
|
+
const { application } = request.app.locals;
|
|
250
|
+
const userId = getUserId(request);
|
|
251
|
+
const { uid } = request.params;
|
|
252
|
+
const { excludeTypes, excludeRegex } = request.body;
|
|
253
|
+
|
|
254
|
+
const channelDocument = await getChannel(application, uid, userId);
|
|
255
|
+
if (!channelDocument) {
|
|
256
|
+
return response.status(404).render("404");
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const validatedTypes = validateExcludeTypes(
|
|
260
|
+
Array.isArray(excludeTypes) ? excludeTypes : [excludeTypes].filter(Boolean),
|
|
261
|
+
);
|
|
262
|
+
const validatedRegex = validateExcludeRegex(excludeRegex);
|
|
263
|
+
|
|
264
|
+
await updateChannelSettings(
|
|
265
|
+
application,
|
|
266
|
+
uid,
|
|
267
|
+
{
|
|
268
|
+
excludeTypes: validatedTypes,
|
|
269
|
+
excludeRegex: validatedRegex,
|
|
270
|
+
},
|
|
271
|
+
userId,
|
|
272
|
+
);
|
|
273
|
+
|
|
274
|
+
response.redirect(`${request.baseUrl}/channels/${uid}`);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Delete channel
|
|
279
|
+
* @param {object} request - Express request
|
|
280
|
+
* @param {object} response - Express response
|
|
281
|
+
* @returns {Promise<void>}
|
|
282
|
+
*/
|
|
283
|
+
export async function deleteChannelAction(request, response) {
|
|
284
|
+
const { application } = request.app.locals;
|
|
285
|
+
const userId = getUserId(request);
|
|
286
|
+
const { uid } = request.params;
|
|
287
|
+
|
|
288
|
+
// Don't allow deleting system channels
|
|
289
|
+
if (uid === "notifications" || uid === "activitypub") {
|
|
290
|
+
return response.redirect(`${request.baseUrl}/channels`);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const channelDocument = await getChannel(application, uid, userId);
|
|
294
|
+
if (!channelDocument) {
|
|
295
|
+
return response.status(404).render("404");
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
await deleteChannel(application, uid, userId);
|
|
299
|
+
|
|
300
|
+
response.redirect(`${request.baseUrl}/channels`);
|
|
301
|
+
}
|