@rmdes/indiekit-endpoint-activitypub 1.0.27 → 1.0.29

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/index.js CHANGED
@@ -149,11 +149,11 @@ export default class ActivityPubEndpoint {
149
149
  router.get("/admin/following", followingController(mp));
150
150
  router.get("/admin/activities", activitiesController(mp));
151
151
  router.get("/admin/featured", featuredGetController(mp));
152
- router.post("/admin/featured/pin", featuredPinController());
153
- router.post("/admin/featured/unpin", featuredUnpinController());
152
+ router.post("/admin/featured/pin", featuredPinController(mp));
153
+ router.post("/admin/featured/unpin", featuredUnpinController(mp));
154
154
  router.get("/admin/tags", featuredTagsGetController(mp));
155
- router.post("/admin/tags/add", featuredTagsAddController());
156
- router.post("/admin/tags/remove", featuredTagsRemoveController());
155
+ router.post("/admin/tags/add", featuredTagsAddController(mp));
156
+ router.post("/admin/tags/remove", featuredTagsRemoveController(mp));
157
157
  router.get("/admin/profile", profileGetController(mp));
158
158
  router.post("/admin/profile", profilePostController(mp));
159
159
  router.get("/admin/migrate", migrateGetController(mp, this.options));
@@ -24,7 +24,7 @@ export function featuredTagsGetController(mountPath) {
24
24
  };
25
25
  }
26
26
 
27
- export function featuredTagsAddController() {
27
+ export function featuredTagsAddController(mountPath) {
28
28
  return async (request, response, next) => {
29
29
  try {
30
30
  const { application } = request.app.locals;
@@ -44,14 +44,14 @@ export function featuredTagsAddController() {
44
44
  { upsert: true },
45
45
  );
46
46
 
47
- response.redirect("back");
47
+ response.redirect(`${mountPath}/admin/tags`);
48
48
  } catch (error) {
49
49
  next(error);
50
50
  }
51
51
  };
52
52
  }
53
53
 
54
- export function featuredTagsRemoveController() {
54
+ export function featuredTagsRemoveController(mountPath) {
55
55
  return async (request, response, next) => {
56
56
  try {
57
57
  const { application } = request.app.locals;
@@ -63,7 +63,7 @@ export function featuredTagsRemoveController() {
63
63
 
64
64
  await collection.deleteOne({ tag });
65
65
 
66
- response.redirect("back");
66
+ response.redirect(`${mountPath}/admin/tags`);
67
67
  } catch (error) {
68
68
  next(error);
69
69
  }
@@ -69,7 +69,7 @@ export function featuredGetController(mountPath) {
69
69
  };
70
70
  }
71
71
 
72
- export function featuredPinController() {
72
+ export function featuredPinController(mountPath) {
73
73
  return async (request, response, next) => {
74
74
  try {
75
75
  const { application } = request.app.locals;
@@ -90,14 +90,14 @@ export function featuredPinController() {
90
90
  { upsert: true },
91
91
  );
92
92
 
93
- response.redirect("back");
93
+ response.redirect(`${mountPath}/admin/featured`);
94
94
  } catch (error) {
95
95
  next(error);
96
96
  }
97
97
  };
98
98
  }
99
99
 
100
- export function featuredUnpinController() {
100
+ export function featuredUnpinController(mountPath) {
101
101
  return async (request, response, next) => {
102
102
  try {
103
103
  const { application } = request.app.locals;
@@ -109,7 +109,7 @@ export function featuredUnpinController() {
109
109
 
110
110
  await collection.deleteOne({ postUrl });
111
111
 
112
- response.redirect("back");
112
+ response.redirect(`${mountPath}/admin/featured`);
113
113
  } catch (error) {
114
114
  next(error);
115
115
  }
@@ -5,6 +5,8 @@
5
5
  * POST: saves updated profile fields back to ap_profile
6
6
  */
7
7
 
8
+ const ACTOR_TYPES = ["Person", "Service", "Organization"];
9
+
8
10
  export function profileGetController(mountPath) {
9
11
  return async (request, response, next) => {
10
12
  try {
@@ -18,6 +20,7 @@ export function profileGetController(mountPath) {
18
20
  title: response.locals.__("activitypub.profile.title"),
19
21
  mountPath,
20
22
  profile,
23
+ actorTypes: ACTOR_TYPES,
21
24
  result: null,
22
25
  });
23
26
  } catch (error) {
@@ -42,10 +45,23 @@ export function profilePostController(mountPath) {
42
45
  url,
43
46
  icon,
44
47
  image,
48
+ actorType,
45
49
  manuallyApprovesFollowers,
46
50
  authorizedFetch,
47
51
  } = request.body;
48
52
 
53
+ // Parse profile links (attachments) from form arrays
54
+ const linkNames = [].concat(request.body["link_name[]"] || []);
55
+ const linkValues = [].concat(request.body["link_value[]"] || []);
56
+ const attachments = [];
57
+ for (let i = 0; i < linkNames.length; i++) {
58
+ const n = linkNames[i]?.trim();
59
+ const v = linkValues[i]?.trim();
60
+ if (n && v) {
61
+ attachments.push({ name: n, value: v });
62
+ }
63
+ }
64
+
49
65
  const update = {
50
66
  $set: {
51
67
  name: name?.trim() || "",
@@ -53,8 +69,10 @@ export function profilePostController(mountPath) {
53
69
  url: url?.trim() || "",
54
70
  icon: icon?.trim() || "",
55
71
  image: image?.trim() || "",
72
+ actorType: ACTOR_TYPES.includes(actorType) ? actorType : "Person",
56
73
  manuallyApprovesFollowers: manuallyApprovesFollowers === "true",
57
74
  authorizedFetch: authorizedFetch === "true",
75
+ attachments,
58
76
  updatedAt: new Date().toISOString(),
59
77
  },
60
78
  };
@@ -67,6 +85,7 @@ export function profilePostController(mountPath) {
67
85
  title: response.locals.__("activitypub.profile.title"),
68
86
  mountPath,
69
87
  profile,
88
+ actorTypes: ACTOR_TYPES,
70
89
  result: {
71
90
  type: "success",
72
91
  text: response.locals.__("activitypub.profile.saved"),
@@ -197,7 +197,11 @@ export function setupFederation(options) {
197
197
  personOptions.published = Temporal.Instant.from(profile.createdAt);
198
198
  }
199
199
 
200
- return new ActorClass(personOptions);
200
+ // Actor type from profile overrides config default
201
+ const profileActorType = profile.actorType || actorType;
202
+ const ResolvedActorClass = actorTypeMap[profileActorType] || ActorClass;
203
+
204
+ return new ResolvedActorClass(personOptions);
201
205
  },
202
206
  )
203
207
  .mapHandle((_ctx, username) => {
package/locales/en.json CHANGED
@@ -38,6 +38,14 @@
38
38
  "imageHint": "URL to a banner image shown at the top of your profile",
39
39
  "manualApprovalLabel": "Manually approve followers",
40
40
  "manualApprovalHint": "When enabled, follow requests require your approval before they take effect",
41
+ "actorTypeLabel": "Actor type",
42
+ "actorTypeHint": "How your account appears in the fediverse. Person for individuals, Service for bots or automated accounts, Organization for groups or companies.",
43
+ "linksLabel": "Profile links",
44
+ "linksHint": "Links shown on your fediverse profile. Add your website, social accounts, or other URLs. Pages that link back with rel=\"me\" will show as verified on Mastodon.",
45
+ "linkNameLabel": "Label",
46
+ "linkValueLabel": "URL",
47
+ "addLink": "Add link",
48
+ "removeLink": "Remove",
41
49
  "authorizedFetchLabel": "Require authorized fetch (secure mode)",
42
50
  "authorizedFetchHint": "When enabled, only servers with valid HTTP Signatures can fetch your actor and collections. This improves privacy but may reduce compatibility with some clients.",
43
51
  "save": "Save profile",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rmdes/indiekit-endpoint-activitypub",
3
- "version": "1.0.27",
3
+ "version": "1.0.29",
4
4
  "description": "ActivityPub federation endpoint for Indiekit via Fedify. Adds full fediverse support: actor, inbox, outbox, followers, following, syndication, and Mastodon migration.",
5
5
  "keywords": [
6
6
  "indiekit",
@@ -4,6 +4,7 @@
4
4
  {% from "input/macro.njk" import input with context %}
5
5
  {% from "textarea/macro.njk" import textarea with context %}
6
6
  {% from "checkboxes/macro.njk" import checkboxes with context %}
7
+ {% from "radios/macro.njk" import radios with context %}
7
8
  {% from "button/macro.njk" import button with context %}
8
9
  {% from "notification-banner/macro.njk" import notificationBanner with context %}
9
10
  {% from "prose/macro.njk" import prose with context %}
@@ -57,6 +58,50 @@
57
58
  type: "url"
58
59
  }) }}
59
60
 
61
+ {{ radios({
62
+ name: "actorType",
63
+ fieldset: {
64
+ legend: __("activitypub.profile.actorTypeLabel")
65
+ },
66
+ hint: __("activitypub.profile.actorTypeHint"),
67
+ items: [{
68
+ label: "Person",
69
+ value: "Person"
70
+ }, {
71
+ label: "Service",
72
+ value: "Service"
73
+ }, {
74
+ label: "Organization",
75
+ value: "Organization"
76
+ }],
77
+ values: [profile.actorType or "Person"]
78
+ }) }}
79
+
80
+ <fieldset class="fieldset" style="margin-block-end: var(--space-l);">
81
+ <legend class="label">{{ __("activitypub.profile.linksLabel") }}</legend>
82
+ <p class="hint">{{ __("activitypub.profile.linksHint") }}</p>
83
+
84
+ <div id="profile-links">
85
+ {% if profile.attachments and profile.attachments.length > 0 %}
86
+ {% for att in profile.attachments %}
87
+ <div class="profile-link-row" style="display: grid; grid-template-columns: 1fr 2fr auto; gap: var(--space-s); align-items: end; margin-block-end: var(--space-s);">
88
+ <div>
89
+ <label class="label" for="link_name_{{ loop.index }}">{{ __("activitypub.profile.linkNameLabel") }}</label>
90
+ <input class="input" type="text" id="link_name_{{ loop.index }}" name="link_name[]" value="{{ att.name }}" placeholder="Website">
91
+ </div>
92
+ <div>
93
+ <label class="label" for="link_value_{{ loop.index }}">{{ __("activitypub.profile.linkValueLabel") }}</label>
94
+ <input class="input" type="url" id="link_value_{{ loop.index }}" name="link_value[]" value="{{ att.value }}" placeholder="https://example.com">
95
+ </div>
96
+ <button type="button" class="button button--small" onclick="this.closest('.profile-link-row').remove()" style="margin-block-end: 4px;">{{ __("activitypub.profile.removeLink") }}</button>
97
+ </div>
98
+ {% endfor %}
99
+ {% endif %}
100
+ </div>
101
+
102
+ <button type="button" class="button button--small" id="add-link-btn">{{ __("activitypub.profile.addLink") }}</button>
103
+ </fieldset>
104
+
60
105
  {{ checkboxes({
61
106
  name: "manuallyApprovesFollowers",
62
107
  items: [
@@ -83,4 +128,57 @@
83
128
 
84
129
  {{ button({ text: __("activitypub.profile.save") }) }}
85
130
  </form>
131
+
132
+ <script>
133
+ (function() {
134
+ var linkCount = {{ (profile.attachments.length if profile.attachments) or 0 }};
135
+ document.getElementById('add-link-btn').addEventListener('click', function() {
136
+ linkCount++;
137
+ var container = document.getElementById('profile-links');
138
+ var row = document.createElement('div');
139
+ row.className = 'profile-link-row';
140
+ row.style.cssText = 'display: grid; grid-template-columns: 1fr 2fr auto; gap: var(--space-s); align-items: end; margin-block-end: var(--space-s);';
141
+
142
+ var nameDiv = document.createElement('div');
143
+ var nameLabel = document.createElement('label');
144
+ nameLabel.className = 'label';
145
+ nameLabel.setAttribute('for', 'link_name_' + linkCount);
146
+ nameLabel.textContent = 'Label';
147
+ var nameInput = document.createElement('input');
148
+ nameInput.className = 'input';
149
+ nameInput.type = 'text';
150
+ nameInput.id = 'link_name_' + linkCount;
151
+ nameInput.name = 'link_name[]';
152
+ nameInput.placeholder = 'Website';
153
+ nameDiv.appendChild(nameLabel);
154
+ nameDiv.appendChild(nameInput);
155
+
156
+ var valueDiv = document.createElement('div');
157
+ var valueLabel = document.createElement('label');
158
+ valueLabel.className = 'label';
159
+ valueLabel.setAttribute('for', 'link_value_' + linkCount);
160
+ valueLabel.textContent = 'URL';
161
+ var valueInput = document.createElement('input');
162
+ valueInput.className = 'input';
163
+ valueInput.type = 'url';
164
+ valueInput.id = 'link_value_' + linkCount;
165
+ valueInput.name = 'link_value[]';
166
+ valueInput.placeholder = 'https://example.com';
167
+ valueDiv.appendChild(valueLabel);
168
+ valueDiv.appendChild(valueInput);
169
+
170
+ var removeBtn = document.createElement('button');
171
+ removeBtn.type = 'button';
172
+ removeBtn.className = 'button button--small';
173
+ removeBtn.style.cssText = 'margin-block-end: 4px;';
174
+ removeBtn.textContent = 'Remove';
175
+ removeBtn.addEventListener('click', function() { row.remove(); });
176
+
177
+ row.appendChild(nameDiv);
178
+ row.appendChild(valueDiv);
179
+ row.appendChild(removeBtn);
180
+ container.appendChild(row);
181
+ });
182
+ })();
183
+ </script>
86
184
  {% endblock %}