@rmdes/indiekit-endpoint-microsub 1.0.0-beta.1
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/README.md +111 -0
- package/index.js +140 -0
- package/lib/cache/redis.js +133 -0
- package/lib/controllers/block.js +85 -0
- package/lib/controllers/channels.js +135 -0
- package/lib/controllers/events.js +56 -0
- package/lib/controllers/follow.js +108 -0
- package/lib/controllers/microsub.js +138 -0
- package/lib/controllers/mute.js +124 -0
- package/lib/controllers/preview.js +67 -0
- package/lib/controllers/reader.js +218 -0
- package/lib/controllers/search.js +142 -0
- package/lib/controllers/timeline.js +117 -0
- package/lib/feeds/atom.js +61 -0
- package/lib/feeds/fetcher.js +205 -0
- package/lib/feeds/hfeed.js +177 -0
- package/lib/feeds/jsonfeed.js +43 -0
- package/lib/feeds/normalizer.js +586 -0
- package/lib/feeds/parser.js +124 -0
- package/lib/feeds/rss.js +61 -0
- package/lib/polling/processor.js +201 -0
- package/lib/polling/scheduler.js +128 -0
- package/lib/polling/tier.js +139 -0
- package/lib/realtime/broker.js +241 -0
- package/lib/search/indexer.js +90 -0
- package/lib/search/query.js +197 -0
- package/lib/storage/channels.js +281 -0
- package/lib/storage/feeds.js +286 -0
- package/lib/storage/filters.js +265 -0
- package/lib/storage/items.js +419 -0
- package/lib/storage/read-state.js +109 -0
- package/lib/utils/jf2.js +170 -0
- package/lib/utils/pagination.js +157 -0
- package/lib/utils/validation.js +217 -0
- package/lib/webmention/processor.js +214 -0
- package/lib/webmention/receiver.js +54 -0
- package/lib/webmention/verifier.js +308 -0
- package/lib/websub/discovery.js +129 -0
- package/lib/websub/handler.js +163 -0
- package/lib/websub/subscriber.js +181 -0
- package/locales/en.json +80 -0
- package/package.json +54 -0
- package/views/channel-new.njk +33 -0
- package/views/channel.njk +41 -0
- package/views/compose.njk +61 -0
- package/views/item.njk +85 -0
- package/views/partials/actions.njk +15 -0
- package/views/partials/author.njk +17 -0
- package/views/partials/item-card.njk +65 -0
- package/views/partials/timeline.njk +10 -0
- package/views/reader.njk +37 -0
- package/views/settings.njk +81 -0
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Follow/unfollow controller
|
|
3
|
+
* @module controllers/follow
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { IndiekitError } from "@indiekit/error";
|
|
7
|
+
|
|
8
|
+
import { refreshFeedNow } from "../polling/scheduler.js";
|
|
9
|
+
import { getChannel } from "../storage/channels.js";
|
|
10
|
+
import {
|
|
11
|
+
createFeed,
|
|
12
|
+
deleteFeed,
|
|
13
|
+
getFeedsForChannel,
|
|
14
|
+
} from "../storage/feeds.js";
|
|
15
|
+
import { createFeedResponse } from "../utils/jf2.js";
|
|
16
|
+
import { validateChannel, validateUrl } from "../utils/validation.js";
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* List followed feeds for a channel
|
|
20
|
+
* GET ?action=follow&channel=<uid>
|
|
21
|
+
* @param {object} request - Express request
|
|
22
|
+
* @param {object} response - Express response
|
|
23
|
+
*/
|
|
24
|
+
export async function list(request, response) {
|
|
25
|
+
const { application } = request.app.locals;
|
|
26
|
+
const userId = request.session?.userId;
|
|
27
|
+
const { channel } = request.query;
|
|
28
|
+
|
|
29
|
+
validateChannel(channel);
|
|
30
|
+
|
|
31
|
+
const channelDocument = await getChannel(application, channel, userId);
|
|
32
|
+
if (!channelDocument) {
|
|
33
|
+
throw new IndiekitError("Channel not found", { status: 404 });
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const feeds = await getFeedsForChannel(application, channelDocument._id);
|
|
37
|
+
const items = feeds.map((feed) => createFeedResponse(feed));
|
|
38
|
+
|
|
39
|
+
response.json({ items });
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Follow a feed URL
|
|
44
|
+
* POST ?action=follow
|
|
45
|
+
* @param {object} request - Express request
|
|
46
|
+
* @param {object} response - Express response
|
|
47
|
+
*/
|
|
48
|
+
export async function follow(request, response) {
|
|
49
|
+
const { application } = request.app.locals;
|
|
50
|
+
const userId = request.session?.userId;
|
|
51
|
+
const { channel, url } = request.body;
|
|
52
|
+
|
|
53
|
+
validateChannel(channel);
|
|
54
|
+
validateUrl(url);
|
|
55
|
+
|
|
56
|
+
const channelDocument = await getChannel(application, channel, userId);
|
|
57
|
+
if (!channelDocument) {
|
|
58
|
+
throw new IndiekitError("Channel not found", { status: 404 });
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Create feed subscription
|
|
62
|
+
const feed = await createFeed(application, {
|
|
63
|
+
channelId: channelDocument._id,
|
|
64
|
+
url,
|
|
65
|
+
title: undefined, // Will be populated on first fetch
|
|
66
|
+
photo: undefined,
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
// Trigger immediate fetch in background (don't await)
|
|
70
|
+
refreshFeedNow(application, feed._id).catch((error) => {
|
|
71
|
+
console.error(`[Microsub] Error fetching new feed ${url}:`, error.message);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
// TODO: Attempt WebSub subscription
|
|
75
|
+
|
|
76
|
+
response.status(201).json(createFeedResponse(feed));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Unfollow a feed URL
|
|
81
|
+
* POST ?action=unfollow
|
|
82
|
+
* @param {object} request - Express request
|
|
83
|
+
* @param {object} response - Express response
|
|
84
|
+
*/
|
|
85
|
+
export async function unfollow(request, response) {
|
|
86
|
+
const { application } = request.app.locals;
|
|
87
|
+
const userId = request.session?.userId;
|
|
88
|
+
const { channel, url } = request.body;
|
|
89
|
+
|
|
90
|
+
validateChannel(channel);
|
|
91
|
+
validateUrl(url);
|
|
92
|
+
|
|
93
|
+
const channelDocument = await getChannel(application, channel, userId);
|
|
94
|
+
if (!channelDocument) {
|
|
95
|
+
throw new IndiekitError("Channel not found", { status: 404 });
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const deleted = await deleteFeed(application, channelDocument._id, url);
|
|
99
|
+
if (!deleted) {
|
|
100
|
+
throw new IndiekitError("Feed not found", { status: 404 });
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// TODO: Cancel WebSub subscription if active
|
|
104
|
+
|
|
105
|
+
response.json({ result: "ok" });
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export const followController = { list, follow, unfollow };
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Main Microsub action router
|
|
3
|
+
* @module controllers/microsub
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { IndiekitError } from "@indiekit/error";
|
|
7
|
+
|
|
8
|
+
import { validateAction } from "../utils/validation.js";
|
|
9
|
+
|
|
10
|
+
import { list as listBlocked, block, unblock } from "./block.js";
|
|
11
|
+
import { list as listChannels, action as channelAction } from "./channels.js";
|
|
12
|
+
import { stream as eventsStream } from "./events.js";
|
|
13
|
+
import { list as listFollows, follow, unfollow } from "./follow.js";
|
|
14
|
+
import { list as listMuted, mute, unmute } from "./mute.js";
|
|
15
|
+
import { get as getPreview, preview } from "./preview.js";
|
|
16
|
+
import { discover, search } from "./search.js";
|
|
17
|
+
import { get as getTimeline, action as timelineAction } from "./timeline.js";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Route GET requests to appropriate action handler
|
|
21
|
+
* @param {object} request - Express request
|
|
22
|
+
* @param {object} response - Express response
|
|
23
|
+
* @param {Function} next - Express next function
|
|
24
|
+
*/
|
|
25
|
+
export async function get(request, response, next) {
|
|
26
|
+
try {
|
|
27
|
+
const { action } = request.query;
|
|
28
|
+
validateAction(action);
|
|
29
|
+
|
|
30
|
+
switch (action) {
|
|
31
|
+
case "channels": {
|
|
32
|
+
return listChannels(request, response);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
case "timeline": {
|
|
36
|
+
return getTimeline(request, response);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
case "follow": {
|
|
40
|
+
return listFollows(request, response);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
case "preview": {
|
|
44
|
+
return getPreview(request, response);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
case "mute": {
|
|
48
|
+
return listMuted(request, response);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
case "block": {
|
|
52
|
+
return listBlocked(request, response);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
case "events": {
|
|
56
|
+
return eventsStream(request, response);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
case "search": {
|
|
60
|
+
// Search is typically POST, but GET is allowed for feed discovery
|
|
61
|
+
return discover(request, response);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
default: {
|
|
65
|
+
throw new IndiekitError(`Unsupported GET action: ${action}`, {
|
|
66
|
+
status: 400,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
} catch (error) {
|
|
71
|
+
next(error);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Route POST requests to appropriate action handler
|
|
77
|
+
* @param {object} request - Express request
|
|
78
|
+
* @param {object} response - Express response
|
|
79
|
+
* @param {Function} next - Express next function
|
|
80
|
+
*/
|
|
81
|
+
export async function post(request, response, next) {
|
|
82
|
+
try {
|
|
83
|
+
const action = request.body.action || request.query.action;
|
|
84
|
+
validateAction(action);
|
|
85
|
+
|
|
86
|
+
switch (action) {
|
|
87
|
+
case "channels": {
|
|
88
|
+
return channelAction(request, response);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
case "timeline": {
|
|
92
|
+
return timelineAction(request, response);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
case "follow": {
|
|
96
|
+
return follow(request, response);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
case "unfollow": {
|
|
100
|
+
return unfollow(request, response);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
case "search": {
|
|
104
|
+
return search(request, response);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
case "preview": {
|
|
108
|
+
return preview(request, response);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
case "mute": {
|
|
112
|
+
return mute(request, response);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
case "unmute": {
|
|
116
|
+
return unmute(request, response);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
case "block": {
|
|
120
|
+
return block(request, response);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
case "unblock": {
|
|
124
|
+
return unblock(request, response);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
default: {
|
|
128
|
+
throw new IndiekitError(`Unsupported POST action: ${action}`, {
|
|
129
|
+
status: 400,
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
} catch (error) {
|
|
134
|
+
next(error);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export const microsubController = { get, post };
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mute controller
|
|
3
|
+
* @module controllers/mute
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { IndiekitError } from "@indiekit/error";
|
|
7
|
+
|
|
8
|
+
import { validateChannel, validateUrl } from "../utils/validation.js";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Get muted collection
|
|
12
|
+
* @param {object} application - Indiekit application
|
|
13
|
+
* @returns {object} MongoDB collection
|
|
14
|
+
*/
|
|
15
|
+
function getCollection(application) {
|
|
16
|
+
return application.collections.get("microsub_muted");
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* List muted URLs for a channel
|
|
21
|
+
* GET ?action=mute&channel=<uid>
|
|
22
|
+
* @param {object} request - Express request
|
|
23
|
+
* @param {object} response - Express response
|
|
24
|
+
*/
|
|
25
|
+
export async function list(request, response) {
|
|
26
|
+
const { application } = request.app.locals;
|
|
27
|
+
const userId = request.session?.userId;
|
|
28
|
+
const { channel } = request.query;
|
|
29
|
+
|
|
30
|
+
// Channel can be "global" or a specific channel UID
|
|
31
|
+
const isGlobal = channel === "global";
|
|
32
|
+
|
|
33
|
+
const collection = getCollection(application);
|
|
34
|
+
const filter = { userId };
|
|
35
|
+
|
|
36
|
+
if (!isGlobal && channel) {
|
|
37
|
+
// Get channel-specific mutes
|
|
38
|
+
const channelsCollection = application.collections.get("microsub_channels");
|
|
39
|
+
const channelDocument = await channelsCollection.findOne({ uid: channel });
|
|
40
|
+
if (channelDocument) {
|
|
41
|
+
filter.channelId = channelDocument._id;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
// For global mutes, we query without channelId (matches all channels)
|
|
45
|
+
|
|
46
|
+
// eslint-disable-next-line unicorn/no-array-callback-reference -- filter is MongoDB query object
|
|
47
|
+
const muted = await collection.find(filter).toArray();
|
|
48
|
+
const items = muted.map((m) => ({ url: m.url }));
|
|
49
|
+
|
|
50
|
+
response.json({ items });
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Mute a URL
|
|
55
|
+
* POST ?action=mute
|
|
56
|
+
* @param {object} request - Express request
|
|
57
|
+
* @param {object} response - Express response
|
|
58
|
+
*/
|
|
59
|
+
export async function mute(request, response) {
|
|
60
|
+
const { application } = request.app.locals;
|
|
61
|
+
const userId = request.session?.userId;
|
|
62
|
+
const { channel, url } = request.body;
|
|
63
|
+
|
|
64
|
+
validateUrl(url);
|
|
65
|
+
|
|
66
|
+
const collection = getCollection(application);
|
|
67
|
+
const isGlobal = channel === "global" || !channel;
|
|
68
|
+
|
|
69
|
+
let channelId;
|
|
70
|
+
if (!isGlobal) {
|
|
71
|
+
validateChannel(channel);
|
|
72
|
+
const channelsCollection = application.collections.get("microsub_channels");
|
|
73
|
+
const channelDocument = await channelsCollection.findOne({ uid: channel });
|
|
74
|
+
if (!channelDocument) {
|
|
75
|
+
throw new IndiekitError("Channel not found", { status: 404 });
|
|
76
|
+
}
|
|
77
|
+
channelId = channelDocument._id;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Check if already muted
|
|
81
|
+
const existing = await collection.findOne({ userId, channelId, url });
|
|
82
|
+
if (!existing) {
|
|
83
|
+
await collection.insertOne({
|
|
84
|
+
userId,
|
|
85
|
+
channelId,
|
|
86
|
+
url,
|
|
87
|
+
createdAt: new Date(),
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
response.json({ result: "ok" });
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Unmute a URL
|
|
96
|
+
* POST ?action=unmute
|
|
97
|
+
* @param {object} request - Express request
|
|
98
|
+
* @param {object} response - Express response
|
|
99
|
+
*/
|
|
100
|
+
export async function unmute(request, response) {
|
|
101
|
+
const { application } = request.app.locals;
|
|
102
|
+
const userId = request.session?.userId;
|
|
103
|
+
const { channel, url } = request.body;
|
|
104
|
+
|
|
105
|
+
validateUrl(url);
|
|
106
|
+
|
|
107
|
+
const collection = getCollection(application);
|
|
108
|
+
const isGlobal = channel === "global" || !channel;
|
|
109
|
+
|
|
110
|
+
let channelId;
|
|
111
|
+
if (!isGlobal) {
|
|
112
|
+
const channelsCollection = application.collections.get("microsub_channels");
|
|
113
|
+
const channelDocument = await channelsCollection.findOne({ uid: channel });
|
|
114
|
+
if (channelDocument) {
|
|
115
|
+
channelId = channelDocument._id;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
await collection.deleteOne({ userId, channelId, url });
|
|
120
|
+
|
|
121
|
+
response.json({ result: "ok" });
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export const muteController = { list, mute, unmute };
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Preview controller
|
|
3
|
+
* @module controllers/preview
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { IndiekitError } from "@indiekit/error";
|
|
7
|
+
|
|
8
|
+
import { fetchAndParseFeed } from "../feeds/fetcher.js";
|
|
9
|
+
import { validateUrl } from "../utils/validation.js";
|
|
10
|
+
|
|
11
|
+
const MAX_PREVIEW_ITEMS = 10;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Fetch and preview a feed
|
|
15
|
+
* @param {string} url - Feed URL
|
|
16
|
+
* @returns {Promise<object>} Preview response
|
|
17
|
+
*/
|
|
18
|
+
async function fetchPreview(url) {
|
|
19
|
+
try {
|
|
20
|
+
const parsed = await fetchAndParseFeed(url);
|
|
21
|
+
|
|
22
|
+
// Return feed metadata and sample items
|
|
23
|
+
return {
|
|
24
|
+
type: "feed",
|
|
25
|
+
url: parsed.url,
|
|
26
|
+
name: parsed.name,
|
|
27
|
+
photo: parsed.photo,
|
|
28
|
+
items: parsed.items.slice(0, MAX_PREVIEW_ITEMS),
|
|
29
|
+
};
|
|
30
|
+
} catch (error) {
|
|
31
|
+
throw new IndiekitError(`Failed to preview feed: ${error.message}`, {
|
|
32
|
+
status: 502,
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Preview a feed URL (GET)
|
|
39
|
+
* GET ?action=preview&url=<feed>
|
|
40
|
+
* @param {object} request - Express request
|
|
41
|
+
* @param {object} response - Express response
|
|
42
|
+
*/
|
|
43
|
+
export async function get(request, response) {
|
|
44
|
+
const { url } = request.query;
|
|
45
|
+
|
|
46
|
+
validateUrl(url);
|
|
47
|
+
|
|
48
|
+
const preview = await fetchPreview(url);
|
|
49
|
+
response.json(preview);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Preview a feed URL (POST)
|
|
54
|
+
* POST ?action=preview
|
|
55
|
+
* @param {object} request - Express request
|
|
56
|
+
* @param {object} response - Express response
|
|
57
|
+
*/
|
|
58
|
+
export async function preview(request, response) {
|
|
59
|
+
const { url } = request.body;
|
|
60
|
+
|
|
61
|
+
validateUrl(url);
|
|
62
|
+
|
|
63
|
+
const previewData = await fetchPreview(url);
|
|
64
|
+
response.json(previewData);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export const previewController = { get, preview };
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reader UI controller
|
|
3
|
+
* @module controllers/reader
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
getChannels,
|
|
8
|
+
getChannel,
|
|
9
|
+
createChannel,
|
|
10
|
+
updateChannelSettings,
|
|
11
|
+
} from "../storage/channels.js";
|
|
12
|
+
import { getTimelineItems, getItemById } from "../storage/items.js";
|
|
13
|
+
import {
|
|
14
|
+
validateChannelName,
|
|
15
|
+
validateExcludeTypes,
|
|
16
|
+
validateExcludeRegex,
|
|
17
|
+
} from "../utils/validation.js";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Reader index - redirect to channels
|
|
21
|
+
* @param {object} request - Express request
|
|
22
|
+
* @param {object} response - Express response
|
|
23
|
+
*/
|
|
24
|
+
export async function index(request, response) {
|
|
25
|
+
response.redirect(`${request.baseUrl}/channels`);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* List channels
|
|
30
|
+
* @param {object} request - Express request
|
|
31
|
+
* @param {object} response - Express response
|
|
32
|
+
*/
|
|
33
|
+
export async function channels(request, response) {
|
|
34
|
+
const { application } = request.app.locals;
|
|
35
|
+
const userId = request.session?.userId;
|
|
36
|
+
|
|
37
|
+
const channelList = await getChannels(application, userId);
|
|
38
|
+
|
|
39
|
+
response.render("reader", {
|
|
40
|
+
title: request.__("microsub.reader.title"),
|
|
41
|
+
channels: channelList,
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* New channel form
|
|
47
|
+
* @param {object} request - Express request
|
|
48
|
+
* @param {object} response - Express response
|
|
49
|
+
*/
|
|
50
|
+
export async function newChannel(request, response) {
|
|
51
|
+
response.render("channel-new", {
|
|
52
|
+
title: request.__("microsub.channels.new"),
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Create channel
|
|
58
|
+
* @param {object} request - Express request
|
|
59
|
+
* @param {object} response - Express response
|
|
60
|
+
*/
|
|
61
|
+
export async function createChannelAction(request, response) {
|
|
62
|
+
const { application } = request.app.locals;
|
|
63
|
+
const userId = request.session?.userId;
|
|
64
|
+
const { name } = request.body;
|
|
65
|
+
|
|
66
|
+
validateChannelName(name);
|
|
67
|
+
|
|
68
|
+
await createChannel(application, { name, userId });
|
|
69
|
+
|
|
70
|
+
response.redirect(`${request.baseUrl}/channels`);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* View channel timeline
|
|
75
|
+
* @param {object} request - Express request
|
|
76
|
+
* @param {object} response - Express response
|
|
77
|
+
*/
|
|
78
|
+
export async function channel(request, response) {
|
|
79
|
+
const { application } = request.app.locals;
|
|
80
|
+
const userId = request.session?.userId;
|
|
81
|
+
const { uid } = request.params;
|
|
82
|
+
const { before, after } = request.query;
|
|
83
|
+
|
|
84
|
+
const channelDocument = await getChannel(application, uid, userId);
|
|
85
|
+
if (!channelDocument) {
|
|
86
|
+
return response.status(404).render("404");
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const timeline = await getTimelineItems(application, channelDocument._id, {
|
|
90
|
+
before,
|
|
91
|
+
after,
|
|
92
|
+
userId,
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
response.render("channel", {
|
|
96
|
+
title: channelDocument.name,
|
|
97
|
+
channel: channelDocument,
|
|
98
|
+
items: timeline.items,
|
|
99
|
+
paging: timeline.paging,
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Channel settings form
|
|
105
|
+
* @param {object} request - Express request
|
|
106
|
+
* @param {object} response - Express response
|
|
107
|
+
*/
|
|
108
|
+
export async function settings(request, response) {
|
|
109
|
+
const { application } = request.app.locals;
|
|
110
|
+
const userId = request.session?.userId;
|
|
111
|
+
const { uid } = request.params;
|
|
112
|
+
|
|
113
|
+
const channelDocument = await getChannel(application, uid, userId);
|
|
114
|
+
if (!channelDocument) {
|
|
115
|
+
return response.status(404).render("404");
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
response.render("settings", {
|
|
119
|
+
title: request.__("microsub.settings.title", {
|
|
120
|
+
channel: channelDocument.name,
|
|
121
|
+
}),
|
|
122
|
+
channel: channelDocument,
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Update channel settings
|
|
128
|
+
* @param {object} request - Express request
|
|
129
|
+
* @param {object} response - Express response
|
|
130
|
+
*/
|
|
131
|
+
export async function updateSettings(request, response) {
|
|
132
|
+
const { application } = request.app.locals;
|
|
133
|
+
const userId = request.session?.userId;
|
|
134
|
+
const { uid } = request.params;
|
|
135
|
+
const { excludeTypes, excludeRegex } = request.body;
|
|
136
|
+
|
|
137
|
+
const channelDocument = await getChannel(application, uid, userId);
|
|
138
|
+
if (!channelDocument) {
|
|
139
|
+
return response.status(404).render("404");
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const validatedTypes = validateExcludeTypes(
|
|
143
|
+
Array.isArray(excludeTypes) ? excludeTypes : [excludeTypes].filter(Boolean),
|
|
144
|
+
);
|
|
145
|
+
const validatedRegex = validateExcludeRegex(excludeRegex);
|
|
146
|
+
|
|
147
|
+
await updateChannelSettings(
|
|
148
|
+
application,
|
|
149
|
+
uid,
|
|
150
|
+
{
|
|
151
|
+
excludeTypes: validatedTypes,
|
|
152
|
+
excludeRegex: validatedRegex,
|
|
153
|
+
},
|
|
154
|
+
userId,
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
response.redirect(`${request.baseUrl}/channels/${uid}`);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* View single item
|
|
162
|
+
* @param {object} request - Express request
|
|
163
|
+
* @param {object} response - Express response
|
|
164
|
+
*/
|
|
165
|
+
export async function item(request, response) {
|
|
166
|
+
const { application } = request.app.locals;
|
|
167
|
+
const userId = request.session?.userId;
|
|
168
|
+
const { id } = request.params;
|
|
169
|
+
|
|
170
|
+
const itemDocument = await getItemById(application, id, userId);
|
|
171
|
+
if (!itemDocument) {
|
|
172
|
+
return response.status(404).render("404");
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
response.render("item", {
|
|
176
|
+
title: itemDocument.name || "Item",
|
|
177
|
+
item: itemDocument,
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Compose response form
|
|
183
|
+
* @param {object} request - Express request
|
|
184
|
+
* @param {object} response - Express response
|
|
185
|
+
*/
|
|
186
|
+
export async function compose(request, response) {
|
|
187
|
+
const { replyTo, likeOf, repostOf } = request.query;
|
|
188
|
+
|
|
189
|
+
response.render("compose", {
|
|
190
|
+
title: request.__("microsub.compose.title"),
|
|
191
|
+
replyTo,
|
|
192
|
+
likeOf,
|
|
193
|
+
repostOf,
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Submit composed response
|
|
199
|
+
* @param {object} request - Express request
|
|
200
|
+
* @param {object} response - Express response
|
|
201
|
+
*/
|
|
202
|
+
export async function submitCompose(request, response) {
|
|
203
|
+
// TODO: Submit via Micropub
|
|
204
|
+
response.redirect(`${request.baseUrl}/channels`);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export const readerController = {
|
|
208
|
+
index,
|
|
209
|
+
channels,
|
|
210
|
+
newChannel,
|
|
211
|
+
createChannel: createChannelAction,
|
|
212
|
+
channel,
|
|
213
|
+
settings,
|
|
214
|
+
updateSettings,
|
|
215
|
+
item,
|
|
216
|
+
compose,
|
|
217
|
+
submitCompose,
|
|
218
|
+
};
|