feat: add actor type selector and profile links to admin UI
- Actor type radio buttons (Person/Service/Organization) in Profile page, stored in ap_profile and read by federation-setup actor dispatcher - Profile links (attachments) section with add/remove for rel="me" verification links, rendered as PropertyValue on the ActivityPub actor - New locale strings for all new UI elements
This commit is contained in:
@@ -5,6 +5,8 @@
|
|||||||
* POST: saves updated profile fields back to ap_profile
|
* POST: saves updated profile fields back to ap_profile
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
const ACTOR_TYPES = ["Person", "Service", "Organization"];
|
||||||
|
|
||||||
export function profileGetController(mountPath) {
|
export function profileGetController(mountPath) {
|
||||||
return async (request, response, next) => {
|
return async (request, response, next) => {
|
||||||
try {
|
try {
|
||||||
@@ -18,6 +20,7 @@ export function profileGetController(mountPath) {
|
|||||||
title: response.locals.__("activitypub.profile.title"),
|
title: response.locals.__("activitypub.profile.title"),
|
||||||
mountPath,
|
mountPath,
|
||||||
profile,
|
profile,
|
||||||
|
actorTypes: ACTOR_TYPES,
|
||||||
result: null,
|
result: null,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -42,10 +45,23 @@ export function profilePostController(mountPath) {
|
|||||||
url,
|
url,
|
||||||
icon,
|
icon,
|
||||||
image,
|
image,
|
||||||
|
actorType,
|
||||||
manuallyApprovesFollowers,
|
manuallyApprovesFollowers,
|
||||||
authorizedFetch,
|
authorizedFetch,
|
||||||
} = request.body;
|
} = request.body;
|
||||||
|
|
||||||
|
// Parse profile links (attachments) from form arrays
|
||||||
|
const linkNames = [].concat(request.body["link_name[]"] || []);
|
||||||
|
const linkValues = [].concat(request.body["link_value[]"] || []);
|
||||||
|
const attachments = [];
|
||||||
|
for (let i = 0; i < linkNames.length; i++) {
|
||||||
|
const n = linkNames[i]?.trim();
|
||||||
|
const v = linkValues[i]?.trim();
|
||||||
|
if (n && v) {
|
||||||
|
attachments.push({ name: n, value: v });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const update = {
|
const update = {
|
||||||
$set: {
|
$set: {
|
||||||
name: name?.trim() || "",
|
name: name?.trim() || "",
|
||||||
@@ -53,8 +69,10 @@ export function profilePostController(mountPath) {
|
|||||||
url: url?.trim() || "",
|
url: url?.trim() || "",
|
||||||
icon: icon?.trim() || "",
|
icon: icon?.trim() || "",
|
||||||
image: image?.trim() || "",
|
image: image?.trim() || "",
|
||||||
|
actorType: ACTOR_TYPES.includes(actorType) ? actorType : "Person",
|
||||||
manuallyApprovesFollowers: manuallyApprovesFollowers === "true",
|
manuallyApprovesFollowers: manuallyApprovesFollowers === "true",
|
||||||
authorizedFetch: authorizedFetch === "true",
|
authorizedFetch: authorizedFetch === "true",
|
||||||
|
attachments,
|
||||||
updatedAt: new Date().toISOString(),
|
updatedAt: new Date().toISOString(),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -67,6 +85,7 @@ export function profilePostController(mountPath) {
|
|||||||
title: response.locals.__("activitypub.profile.title"),
|
title: response.locals.__("activitypub.profile.title"),
|
||||||
mountPath,
|
mountPath,
|
||||||
profile,
|
profile,
|
||||||
|
actorTypes: ACTOR_TYPES,
|
||||||
result: {
|
result: {
|
||||||
type: "success",
|
type: "success",
|
||||||
text: response.locals.__("activitypub.profile.saved"),
|
text: response.locals.__("activitypub.profile.saved"),
|
||||||
|
|||||||
@@ -197,7 +197,11 @@ export function setupFederation(options) {
|
|||||||
personOptions.published = Temporal.Instant.from(profile.createdAt);
|
personOptions.published = Temporal.Instant.from(profile.createdAt);
|
||||||
}
|
}
|
||||||
|
|
||||||
return new ActorClass(personOptions);
|
// Actor type from profile overrides config default
|
||||||
|
const profileActorType = profile.actorType || actorType;
|
||||||
|
const ResolvedActorClass = actorTypeMap[profileActorType] || ActorClass;
|
||||||
|
|
||||||
|
return new ResolvedActorClass(personOptions);
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.mapHandle((_ctx, username) => {
|
.mapHandle((_ctx, username) => {
|
||||||
|
|||||||
@@ -38,6 +38,14 @@
|
|||||||
"imageHint": "URL to a banner image shown at the top of your profile",
|
"imageHint": "URL to a banner image shown at the top of your profile",
|
||||||
"manualApprovalLabel": "Manually approve followers",
|
"manualApprovalLabel": "Manually approve followers",
|
||||||
"manualApprovalHint": "When enabled, follow requests require your approval before they take effect",
|
"manualApprovalHint": "When enabled, follow requests require your approval before they take effect",
|
||||||
|
"actorTypeLabel": "Actor type",
|
||||||
|
"actorTypeHint": "How your account appears in the fediverse. Person for individuals, Service for bots or automated accounts, Organization for groups or companies.",
|
||||||
|
"linksLabel": "Profile links",
|
||||||
|
"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.",
|
||||||
|
"linkNameLabel": "Label",
|
||||||
|
"linkValueLabel": "URL",
|
||||||
|
"addLink": "Add link",
|
||||||
|
"removeLink": "Remove",
|
||||||
"authorizedFetchLabel": "Require authorized fetch (secure mode)",
|
"authorizedFetchLabel": "Require authorized fetch (secure mode)",
|
||||||
"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.",
|
"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.",
|
||||||
"save": "Save profile",
|
"save": "Save profile",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@rmdes/indiekit-endpoint-activitypub",
|
"name": "@rmdes/indiekit-endpoint-activitypub",
|
||||||
"version": "1.0.28",
|
"version": "1.0.29",
|
||||||
"description": "ActivityPub federation endpoint for Indiekit via Fedify. Adds full fediverse support: actor, inbox, outbox, followers, following, syndication, and Mastodon migration.",
|
"description": "ActivityPub federation endpoint for Indiekit via Fedify. Adds full fediverse support: actor, inbox, outbox, followers, following, syndication, and Mastodon migration.",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"indiekit",
|
"indiekit",
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
{% from "input/macro.njk" import input with context %}
|
{% from "input/macro.njk" import input with context %}
|
||||||
{% from "textarea/macro.njk" import textarea with context %}
|
{% from "textarea/macro.njk" import textarea with context %}
|
||||||
{% from "checkboxes/macro.njk" import checkboxes with context %}
|
{% from "checkboxes/macro.njk" import checkboxes with context %}
|
||||||
|
{% from "radios/macro.njk" import radios with context %}
|
||||||
{% from "button/macro.njk" import button with context %}
|
{% from "button/macro.njk" import button with context %}
|
||||||
{% from "notification-banner/macro.njk" import notificationBanner with context %}
|
{% from "notification-banner/macro.njk" import notificationBanner with context %}
|
||||||
{% from "prose/macro.njk" import prose with context %}
|
{% from "prose/macro.njk" import prose with context %}
|
||||||
@@ -57,6 +58,50 @@
|
|||||||
type: "url"
|
type: "url"
|
||||||
}) }}
|
}) }}
|
||||||
|
|
||||||
|
{{ radios({
|
||||||
|
name: "actorType",
|
||||||
|
fieldset: {
|
||||||
|
legend: __("activitypub.profile.actorTypeLabel")
|
||||||
|
},
|
||||||
|
hint: __("activitypub.profile.actorTypeHint"),
|
||||||
|
items: [{
|
||||||
|
label: "Person",
|
||||||
|
value: "Person"
|
||||||
|
}, {
|
||||||
|
label: "Service",
|
||||||
|
value: "Service"
|
||||||
|
}, {
|
||||||
|
label: "Organization",
|
||||||
|
value: "Organization"
|
||||||
|
}],
|
||||||
|
values: [profile.actorType or "Person"]
|
||||||
|
}) }}
|
||||||
|
|
||||||
|
<fieldset class="fieldset" style="margin-block-end: var(--space-l);">
|
||||||
|
<legend class="label">{{ __("activitypub.profile.linksLabel") }}</legend>
|
||||||
|
<p class="hint">{{ __("activitypub.profile.linksHint") }}</p>
|
||||||
|
|
||||||
|
<div id="profile-links">
|
||||||
|
{% if profile.attachments and profile.attachments.length > 0 %}
|
||||||
|
{% for att in profile.attachments %}
|
||||||
|
<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);">
|
||||||
|
<div>
|
||||||
|
<label class="label" for="link_name_{{ loop.index }}">{{ __("activitypub.profile.linkNameLabel") }}</label>
|
||||||
|
<input class="input" type="text" id="link_name_{{ loop.index }}" name="link_name[]" value="{{ att.name }}" placeholder="Website">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="label" for="link_value_{{ loop.index }}">{{ __("activitypub.profile.linkValueLabel") }}</label>
|
||||||
|
<input class="input" type="url" id="link_value_{{ loop.index }}" name="link_value[]" value="{{ att.value }}" placeholder="https://example.com">
|
||||||
|
</div>
|
||||||
|
<button type="button" class="button button--small" onclick="this.closest('.profile-link-row').remove()" style="margin-block-end: 4px;">{{ __("activitypub.profile.removeLink") }}</button>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="button" class="button button--small" id="add-link-btn">{{ __("activitypub.profile.addLink") }}</button>
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
{{ checkboxes({
|
{{ checkboxes({
|
||||||
name: "manuallyApprovesFollowers",
|
name: "manuallyApprovesFollowers",
|
||||||
items: [
|
items: [
|
||||||
@@ -83,4 +128,57 @@
|
|||||||
|
|
||||||
{{ button({ text: __("activitypub.profile.save") }) }}
|
{{ button({ text: __("activitypub.profile.save") }) }}
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
(function() {
|
||||||
|
var linkCount = {{ (profile.attachments.length if profile.attachments) or 0 }};
|
||||||
|
document.getElementById('add-link-btn').addEventListener('click', function() {
|
||||||
|
linkCount++;
|
||||||
|
var container = document.getElementById('profile-links');
|
||||||
|
var row = document.createElement('div');
|
||||||
|
row.className = 'profile-link-row';
|
||||||
|
row.style.cssText = 'display: grid; grid-template-columns: 1fr 2fr auto; gap: var(--space-s); align-items: end; margin-block-end: var(--space-s);';
|
||||||
|
|
||||||
|
var nameDiv = document.createElement('div');
|
||||||
|
var nameLabel = document.createElement('label');
|
||||||
|
nameLabel.className = 'label';
|
||||||
|
nameLabel.setAttribute('for', 'link_name_' + linkCount);
|
||||||
|
nameLabel.textContent = 'Label';
|
||||||
|
var nameInput = document.createElement('input');
|
||||||
|
nameInput.className = 'input';
|
||||||
|
nameInput.type = 'text';
|
||||||
|
nameInput.id = 'link_name_' + linkCount;
|
||||||
|
nameInput.name = 'link_name[]';
|
||||||
|
nameInput.placeholder = 'Website';
|
||||||
|
nameDiv.appendChild(nameLabel);
|
||||||
|
nameDiv.appendChild(nameInput);
|
||||||
|
|
||||||
|
var valueDiv = document.createElement('div');
|
||||||
|
var valueLabel = document.createElement('label');
|
||||||
|
valueLabel.className = 'label';
|
||||||
|
valueLabel.setAttribute('for', 'link_value_' + linkCount);
|
||||||
|
valueLabel.textContent = 'URL';
|
||||||
|
var valueInput = document.createElement('input');
|
||||||
|
valueInput.className = 'input';
|
||||||
|
valueInput.type = 'url';
|
||||||
|
valueInput.id = 'link_value_' + linkCount;
|
||||||
|
valueInput.name = 'link_value[]';
|
||||||
|
valueInput.placeholder = 'https://example.com';
|
||||||
|
valueDiv.appendChild(valueLabel);
|
||||||
|
valueDiv.appendChild(valueInput);
|
||||||
|
|
||||||
|
var removeBtn = document.createElement('button');
|
||||||
|
removeBtn.type = 'button';
|
||||||
|
removeBtn.className = 'button button--small';
|
||||||
|
removeBtn.style.cssText = 'margin-block-end: 4px;';
|
||||||
|
removeBtn.textContent = 'Remove';
|
||||||
|
removeBtn.addEventListener('click', function() { row.remove(); });
|
||||||
|
|
||||||
|
row.appendChild(nameDiv);
|
||||||
|
row.appendChild(valueDiv);
|
||||||
|
row.appendChild(removeBtn);
|
||||||
|
container.appendChild(row);
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
Reference in New Issue
Block a user