@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.
Files changed (52) hide show
  1. package/README.md +111 -0
  2. package/index.js +140 -0
  3. package/lib/cache/redis.js +133 -0
  4. package/lib/controllers/block.js +85 -0
  5. package/lib/controllers/channels.js +135 -0
  6. package/lib/controllers/events.js +56 -0
  7. package/lib/controllers/follow.js +108 -0
  8. package/lib/controllers/microsub.js +138 -0
  9. package/lib/controllers/mute.js +124 -0
  10. package/lib/controllers/preview.js +67 -0
  11. package/lib/controllers/reader.js +218 -0
  12. package/lib/controllers/search.js +142 -0
  13. package/lib/controllers/timeline.js +117 -0
  14. package/lib/feeds/atom.js +61 -0
  15. package/lib/feeds/fetcher.js +205 -0
  16. package/lib/feeds/hfeed.js +177 -0
  17. package/lib/feeds/jsonfeed.js +43 -0
  18. package/lib/feeds/normalizer.js +586 -0
  19. package/lib/feeds/parser.js +124 -0
  20. package/lib/feeds/rss.js +61 -0
  21. package/lib/polling/processor.js +201 -0
  22. package/lib/polling/scheduler.js +128 -0
  23. package/lib/polling/tier.js +139 -0
  24. package/lib/realtime/broker.js +241 -0
  25. package/lib/search/indexer.js +90 -0
  26. package/lib/search/query.js +197 -0
  27. package/lib/storage/channels.js +281 -0
  28. package/lib/storage/feeds.js +286 -0
  29. package/lib/storage/filters.js +265 -0
  30. package/lib/storage/items.js +419 -0
  31. package/lib/storage/read-state.js +109 -0
  32. package/lib/utils/jf2.js +170 -0
  33. package/lib/utils/pagination.js +157 -0
  34. package/lib/utils/validation.js +217 -0
  35. package/lib/webmention/processor.js +214 -0
  36. package/lib/webmention/receiver.js +54 -0
  37. package/lib/webmention/verifier.js +308 -0
  38. package/lib/websub/discovery.js +129 -0
  39. package/lib/websub/handler.js +163 -0
  40. package/lib/websub/subscriber.js +181 -0
  41. package/locales/en.json +80 -0
  42. package/package.json +54 -0
  43. package/views/channel-new.njk +33 -0
  44. package/views/channel.njk +41 -0
  45. package/views/compose.njk +61 -0
  46. package/views/item.njk +85 -0
  47. package/views/partials/actions.njk +15 -0
  48. package/views/partials/author.njk +17 -0
  49. package/views/partials/item-card.njk +65 -0
  50. package/views/partials/timeline.njk +10 -0
  51. package/views/reader.njk +37 -0
  52. package/views/settings.njk +81 -0
package/views/item.njk ADDED
@@ -0,0 +1,85 @@
1
+ {% extends "document.njk" %}
2
+
3
+ {% block content %}
4
+ <article class="item">
5
+ <a href="{{ request.headers.referer or request.baseUrl + '/channels' }}" class="back-link">
6
+ {{ icon("arrow-left") }} {{ __("Back") }}
7
+ </a>
8
+
9
+ {% if item.author %}
10
+ <header class="item__author">
11
+ {% if item.author.photo %}
12
+ <img src="{{ item.author.photo }}" alt="" class="item__author-photo" width="48" height="48" loading="lazy">
13
+ {% endif %}
14
+ <div class="item__author-info">
15
+ <span class="item__author-name">
16
+ {% if item.author.url %}
17
+ <a href="{{ item.author.url }}">{{ item.author.name or item.author.url }}</a>
18
+ {% else %}
19
+ {{ item.author.name or "Unknown" }}
20
+ {% endif %}
21
+ </span>
22
+ {% if item.published %}
23
+ <time datetime="{{ item.published }}" class="item__date">
24
+ {{ item.published | date("PPPp", { locale: locale, timeZone: application.timeZone }) }}
25
+ </time>
26
+ {% endif %}
27
+ </div>
28
+ </header>
29
+ {% endif %}
30
+
31
+ {% if item.name %}
32
+ <h2 class="item__title">{{ item.name }}</h2>
33
+ {% endif %}
34
+
35
+ {% if item.content %}
36
+ <div class="item__content prose">
37
+ {% if item.content.html %}
38
+ {{ item.content.html | safe }}
39
+ {% else %}
40
+ {{ item.content.text }}
41
+ {% endif %}
42
+ </div>
43
+ {% endif %}
44
+
45
+ {% if item.photo and item.photo.length > 0 %}
46
+ <div class="item__photos">
47
+ {% for photo in item.photo %}
48
+ <img src="{{ photo }}" alt="" class="item__photo" loading="lazy">
49
+ {% endfor %}
50
+ </div>
51
+ {% endif %}
52
+
53
+ {% if item.inReplyTo or item.likeOf or item.repostOf or item.bookmarkOf %}
54
+ <div class="item__context">
55
+ {% if item.inReplyTo %}
56
+ <p>{{ icon("reply") }} {{ __("Reply to") }}: <a href="{{ item.inReplyTo[0] }}">{{ item.inReplyTo[0] }}</a></p>
57
+ {% endif %}
58
+ {% if item.likeOf %}
59
+ <p>{{ icon("heart") }} {{ __("Liked") }}: <a href="{{ item.likeOf[0] }}">{{ item.likeOf[0] }}</a></p>
60
+ {% endif %}
61
+ {% if item.repostOf %}
62
+ <p>{{ icon("repost") }} {{ __("Reposted") }}: <a href="{{ item.repostOf[0] }}">{{ item.repostOf[0] }}</a></p>
63
+ {% endif %}
64
+ {% if item.bookmarkOf %}
65
+ <p>{{ icon("bookmark") }} {{ __("Bookmarked") }}: <a href="{{ item.bookmarkOf[0] }}">{{ item.bookmarkOf[0] }}</a></p>
66
+ {% endif %}
67
+ </div>
68
+ {% endif %}
69
+
70
+ <footer class="item__actions">
71
+ <a href="{{ request.baseUrl }}/compose?replyTo={{ item.url | urlencode }}" class="button button--secondary button--small">
72
+ {{ icon("reply") }} {{ __("microsub.item.reply") }}
73
+ </a>
74
+ <a href="{{ request.baseUrl }}/compose?likeOf={{ item.url | urlencode }}" class="button button--secondary button--small">
75
+ {{ icon("heart") }} {{ __("microsub.item.like") }}
76
+ </a>
77
+ <a href="{{ request.baseUrl }}/compose?repostOf={{ item.url | urlencode }}" class="button button--secondary button--small">
78
+ {{ icon("repost") }} {{ __("microsub.item.repost") }}
79
+ </a>
80
+ <a href="{{ item.url }}" class="button button--secondary button--small" target="_blank" rel="noopener">
81
+ {{ icon("external") }} {{ __("microsub.item.viewOriginal") }}
82
+ </a>
83
+ </footer>
84
+ </article>
85
+ {% endblock %}
@@ -0,0 +1,15 @@
1
+ {# Item action buttons #}
2
+ <div class="item-actions">
3
+ <a href="{{ request.baseUrl }}/compose?replyTo={{ itemUrl | urlencode }}" class="item-actions__button" title="{{ __('microsub.item.reply') }}">
4
+ {{ icon("reply") }}
5
+ </a>
6
+ <a href="{{ request.baseUrl }}/compose?likeOf={{ itemUrl | urlencode }}" class="item-actions__button" title="{{ __('microsub.item.like') }}">
7
+ {{ icon("heart") }}
8
+ </a>
9
+ <a href="{{ request.baseUrl }}/compose?repostOf={{ itemUrl | urlencode }}" class="item-actions__button" title="{{ __('microsub.item.repost') }}">
10
+ {{ icon("repost") }}
11
+ </a>
12
+ <a href="{{ itemUrl }}" class="item-actions__button" target="_blank" rel="noopener" title="{{ __('microsub.item.viewOriginal') }}">
13
+ {{ icon("external") }}
14
+ </a>
15
+ </div>
@@ -0,0 +1,17 @@
1
+ {# Author display #}
2
+ {% if author %}
3
+ <div class="author">
4
+ {% if author.photo %}
5
+ <img src="{{ author.photo }}" alt="" class="author__photo" width="48" height="48" loading="lazy">
6
+ {% endif %}
7
+ <div class="author__info">
8
+ <span class="author__name">
9
+ {% if author.url %}
10
+ <a href="{{ author.url }}">{{ author.name or author.url }}</a>
11
+ {% else %}
12
+ {{ author.name or "Unknown" }}
13
+ {% endif %}
14
+ </span>
15
+ </div>
16
+ </div>
17
+ {% endif %}
@@ -0,0 +1,65 @@
1
+ {# Item card for timeline display #}
2
+ <article class="item-card{% if item._is_read %} item-card--read{% endif %}">
3
+ <a href="{{ request.baseUrl }}/item/{{ item.uid }}" class="item-card__link">
4
+ {% if item.author %}
5
+ <div class="item-card__author">
6
+ {% if item.author.photo %}
7
+ <img src="{{ item.author.photo }}" alt="" class="item-card__author-photo" width="40" height="40" loading="lazy">
8
+ {% endif %}
9
+ <div class="item-card__author-info">
10
+ <span class="item-card__author-name">{{ item.author.name or "Unknown" }}</span>
11
+ {% if item._source %}
12
+ <span class="item-card__source">{{ item._source.name or item._source.url }}</span>
13
+ {% endif %}
14
+ </div>
15
+ </div>
16
+ {% endif %}
17
+
18
+ {% if item._type and item._type !== "entry" %}
19
+ <div class="item-card__type">
20
+ {% if item._type === "like" %}
21
+ {{ icon("heart") }} Liked
22
+ {% elif item._type === "repost" %}
23
+ {{ icon("repost") }} Reposted
24
+ {% elif item._type === "reply" %}
25
+ {{ icon("reply") }} Reply
26
+ {% elif item._type === "bookmark" %}
27
+ {{ icon("bookmark") }} Bookmarked
28
+ {% endif %}
29
+ </div>
30
+ {% endif %}
31
+
32
+ {% if item.name %}
33
+ <h3 class="item-card__title">{{ item.name }}</h3>
34
+ {% endif %}
35
+
36
+ {% if item.summary or item.content %}
37
+ <div class="item-card__content">
38
+ {% if item.summary %}
39
+ {{ item.summary | truncate(200) }}
40
+ {% elif item.content.text %}
41
+ {{ item.content.text | truncate(200) }}
42
+ {% endif %}
43
+ </div>
44
+ {% endif %}
45
+
46
+ {% if item.photo and item.photo.length > 0 %}
47
+ <div class="item-card__photos">
48
+ {% for photo in item.photo | slice(0, 4) %}
49
+ <img src="{{ photo }}" alt="" class="item-card__photo" loading="lazy">
50
+ {% endfor %}
51
+ </div>
52
+ {% endif %}
53
+
54
+ <footer class="item-card__footer">
55
+ {% if item.published %}
56
+ <time datetime="{{ item.published }}" class="item-card__date">
57
+ {{ item.published | date("PP", { locale: locale, timeZone: application.timeZone }) }}
58
+ </time>
59
+ {% endif %}
60
+ {% if not item._is_read %}
61
+ <span class="item-card__unread">{{ icon("dot") }}</span>
62
+ {% endif %}
63
+ </footer>
64
+ </a>
65
+ </article>
@@ -0,0 +1,10 @@
1
+ {# Timeline of items #}
2
+ <div class="timeline">
3
+ {% if items.length > 0 %}
4
+ {% for item in items %}
5
+ {% include "partials/item-card.njk" %}
6
+ {% endfor %}
7
+ {% else %}
8
+ {{ prose({ text: __("microsub.timeline.empty") }) }}
9
+ {% endif %}
10
+ </div>
@@ -0,0 +1,37 @@
1
+ {% extends "document.njk" %}
2
+
3
+ {% block content %}
4
+ <div class="reader">
5
+ {% if channels.length > 0 %}
6
+ <ul class="reader__channels">
7
+ {% for channel in channels %}
8
+ <li class="reader__channel">
9
+ <a href="{{ request.baseUrl }}/channels/{{ channel.uid }}" class="reader__channel-link">
10
+ <span class="reader__channel-name">
11
+ {% if channel.uid === "notifications" %}
12
+ {{ icon("bell") }}
13
+ {% endif %}
14
+ {{ channel.name }}
15
+ </span>
16
+ {% if channel.unread > 0 %}
17
+ <span class="reader__channel-badge">{{ channel.unread }}</span>
18
+ {% endif %}
19
+ </a>
20
+ </li>
21
+ {% endfor %}
22
+ </ul>
23
+ <p class="reader__actions">
24
+ <a href="{{ request.baseUrl }}/channels/new" class="button button--secondary">
25
+ {{ icon("plus") }} {{ __("microsub.channels.new") }}
26
+ </a>
27
+ </p>
28
+ {% else %}
29
+ {{ prose({ text: __("microsub.channels.empty") }) }}
30
+ <p>
31
+ <a href="{{ request.baseUrl }}/channels/new" class="button button--primary">
32
+ {{ __("microsub.channels.new") }}
33
+ </a>
34
+ </p>
35
+ {% endif %}
36
+ </div>
37
+ {% endblock %}
@@ -0,0 +1,81 @@
1
+ {% extends "document.njk" %}
2
+
3
+ {% block content %}
4
+ <div class="settings">
5
+ <a href="{{ request.baseUrl }}/channels/{{ channel.uid }}" class="back-link">
6
+ {{ icon("arrow-left") }} {{ channel.name }}
7
+ </a>
8
+
9
+ <form method="post" action="{{ request.baseUrl }}/channels/{{ channel.uid }}/settings">
10
+ {{ fieldset({
11
+ legend: {
12
+ text: __("microsub.settings.excludeTypes"),
13
+ classes: "fieldset__legend--medium"
14
+ },
15
+ hint: {
16
+ text: __("microsub.settings.excludeTypesHelp")
17
+ },
18
+ items: [
19
+ {
20
+ id: "excludeTypes-like",
21
+ name: "excludeTypes",
22
+ value: "like",
23
+ text: __("microsub.settings.types.like"),
24
+ checked: channel.settings.excludeTypes | includes("like")
25
+ },
26
+ {
27
+ id: "excludeTypes-repost",
28
+ name: "excludeTypes",
29
+ value: "repost",
30
+ text: __("microsub.settings.types.repost"),
31
+ checked: channel.settings.excludeTypes | includes("repost")
32
+ },
33
+ {
34
+ id: "excludeTypes-bookmark",
35
+ name: "excludeTypes",
36
+ value: "bookmark",
37
+ text: __("microsub.settings.types.bookmark"),
38
+ checked: channel.settings.excludeTypes | includes("bookmark")
39
+ },
40
+ {
41
+ id: "excludeTypes-reply",
42
+ name: "excludeTypes",
43
+ value: "reply",
44
+ text: __("microsub.settings.types.reply"),
45
+ checked: channel.settings.excludeTypes | includes("reply")
46
+ },
47
+ {
48
+ id: "excludeTypes-checkin",
49
+ name: "excludeTypes",
50
+ value: "checkin",
51
+ text: __("microsub.settings.types.checkin"),
52
+ checked: channel.settings.excludeTypes | includes("checkin")
53
+ }
54
+ ]
55
+ }) | checkboxes }}
56
+
57
+ {{ field({
58
+ label: {
59
+ text: __("microsub.settings.excludeRegex")
60
+ },
61
+ hint: {
62
+ text: __("microsub.settings.excludeRegexHelp")
63
+ },
64
+ input: {
65
+ id: "excludeRegex",
66
+ name: "excludeRegex",
67
+ value: channel.settings.excludeRegex
68
+ }
69
+ }) }}
70
+
71
+ <div class="button-group">
72
+ {{ button({
73
+ text: __("microsub.settings.save")
74
+ }) }}
75
+ <a href="{{ request.baseUrl }}/channels/{{ channel.uid }}" class="button button--secondary">
76
+ {{ __("Cancel") }}
77
+ </a>
78
+ </div>
79
+ </form>
80
+ </div>
81
+ {% endblock %}