mirror of
https://github.com/svemagie/indiekit-endpoint-activitypub.git
synced 2026-04-02 15:44:58 +02:00
Merge upstream rmdes:main — v2.13.0–v2.15.4 into svemagie/main
New upstream features:
- v2.13.0: FEP-8fcf/fe34 compliance, custom emoji, manual follow approval
- v2.14.0: Server blocking, Redis caching, key refresh, async inbox queue
- v2.15.0: Outbox failure handling (strike system), reply chain forwarding
- v2.15.1: Reply intelligence in reader (visibility badges, thread reconstruction)
- v2.15.2: Strip invalid as:Endpoints type from actor serialization
- v2.15.3: Exclude soft-deleted posts from outbox/content negotiation
- v2.15.4: Wire content-warning property for CW text
Conflict resolution:
- federation-setup.js: merged our draft/unlisted/visibility filters with
upstream's soft-delete filter
- compose.js: kept our DM compose path, adopted upstream's
lookupWithSecurity for remote object resolution
- notifications.js: kept our separate reply/mention tabs, added upstream's
follow_request grouping
- inbox-listeners.js: took upstream's thin-shim rewrite (handlers moved to
inbox-handlers.js which already has DM detection)
- notification-card.njk: merged DM badge with follow_request support
Preserved from our fork:
- Like/Announce to:Public cc:followers addressing
- Nested tag normalization (cat.split("/").at(-1))
- DM compose/reply path in compose controller
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -6,19 +6,67 @@
|
||||
{% from "pagination/macro.njk" import pagination with context %}
|
||||
|
||||
{% block content %}
|
||||
{% if followers.length > 0 %}
|
||||
{% for follower in followers %}
|
||||
{{ card({
|
||||
title: follower.name or follower.handle or follower.actorUrl,
|
||||
url: follower.actorUrl,
|
||||
photo: { url: follower.avatar, alt: follower.name } if follower.avatar,
|
||||
description: { text: "@" + follower.handle if follower.handle },
|
||||
published: follower.followedAt
|
||||
}) }}
|
||||
{% endfor %}
|
||||
{# Tab navigation — only show if there are pending requests #}
|
||||
{% if pendingCount > 0 %}
|
||||
{% set followersBase = mountPath + "/admin/followers" %}
|
||||
<nav class="ap-tabs">
|
||||
<a href="{{ followersBase }}" class="ap-tab{% if tab == 'followers' %} ap-tab--active{% endif %}">
|
||||
{{ __("activitypub.followers") }}
|
||||
{% if followerCount %}<span class="ap-tab__count">{{ followerCount }}</span>{% endif %}
|
||||
</a>
|
||||
<a href="{{ followersBase }}?tab=pending" class="ap-tab{% if tab == 'pending' %} ap-tab--active{% endif %}">
|
||||
{{ __("activitypub.pendingFollows") }}
|
||||
<span class="ap-tab__count">{{ pendingCount }}</span>
|
||||
</a>
|
||||
</nav>
|
||||
{% endif %}
|
||||
|
||||
{{ pagination(cursor) if cursor }}
|
||||
{% if tab == "pending" %}
|
||||
{# Pending follow requests #}
|
||||
{% if pendingFollows.length > 0 %}
|
||||
{% for pending in pendingFollows %}
|
||||
<div class="ap-follow-request">
|
||||
{{ card({
|
||||
title: pending.name or pending.handle or pending.actorUrl,
|
||||
url: pending.actorUrl,
|
||||
photo: { url: pending.avatar, alt: pending.name } if pending.avatar,
|
||||
description: { text: "@" + pending.handle if pending.handle }
|
||||
}) }}
|
||||
<div class="ap-follow-request__actions">
|
||||
<form method="post" action="{{ mountPath }}/admin/followers/approve" class="ap-follow-request__form">
|
||||
<input type="hidden" name="_csrf" value="{{ csrfToken }}">
|
||||
<input type="hidden" name="actorUrl" value="{{ pending.actorUrl }}">
|
||||
<button type="submit" class="button">{{ __("activitypub.approve") }}</button>
|
||||
</form>
|
||||
<form method="post" action="{{ mountPath }}/admin/followers/reject" class="ap-follow-request__form">
|
||||
<input type="hidden" name="_csrf" value="{{ csrfToken }}">
|
||||
<input type="hidden" name="actorUrl" value="{{ pending.actorUrl }}">
|
||||
<button type="submit" class="button button--danger">{{ __("activitypub.reject") }}</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
{{ pagination(cursor) if cursor }}
|
||||
{% else %}
|
||||
{{ prose({ text: __("activitypub.noPendingFollows") }) }}
|
||||
{% endif %}
|
||||
{% else %}
|
||||
{{ prose({ text: __("activitypub.noFollowers") }) }}
|
||||
{# Accepted followers #}
|
||||
{% if followers.length > 0 %}
|
||||
{% for follower in followers %}
|
||||
{{ card({
|
||||
title: follower.name or follower.handle or follower.actorUrl,
|
||||
url: follower.actorUrl,
|
||||
photo: { url: follower.avatar, alt: follower.name } if follower.avatar,
|
||||
description: { text: "@" + follower.handle if follower.handle },
|
||||
published: follower.followedAt
|
||||
}) }}
|
||||
{% endfor %}
|
||||
|
||||
{{ pagination(cursor) if cursor }}
|
||||
{% else %}
|
||||
{{ prose({ text: __("activitypub.noFollowers") }) }}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
@@ -26,6 +26,38 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{# Blocked servers #}
|
||||
<section class="ap-moderation__section">
|
||||
<h2>Blocked Servers</h2>
|
||||
<p class="ap-moderation__hint">Block entire instances by hostname. Activities from blocked servers are rejected before any processing.</p>
|
||||
{% if blockedServers and blockedServers.length > 0 %}
|
||||
<ul class="ap-moderation__list" x-ref="serverList">
|
||||
{% for entry in blockedServers %}
|
||||
<li class="ap-moderation__entry" data-hostname="{{ entry.hostname }}">
|
||||
<code>{{ entry.hostname }}</code>
|
||||
{% if entry.reason %}<span class="ap-moderation__reason">({{ entry.reason }})</span>{% endif %}
|
||||
<button class="ap-moderation__remove"
|
||||
@click="removeEntry($el, 'unblock-server', { hostname: $el.closest('li').dataset.hostname })">
|
||||
Unblock
|
||||
</button>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% else %}
|
||||
<p class="ap-moderation__empty" x-ref="serverEmpty">No servers blocked.</p>
|
||||
{% endif %}
|
||||
|
||||
<form class="ap-moderation__add-form" @submit.prevent="addBlockedServer()">
|
||||
<input type="text" x-model="newServerHostname"
|
||||
placeholder="spam.instance.social"
|
||||
class="ap-moderation__input"
|
||||
x-ref="serverInput">
|
||||
<button type="submit" :disabled="submitting" class="ap-moderation__add-btn">
|
||||
Block Server
|
||||
</button>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
{# Blocked actors #}
|
||||
<section class="ap-moderation__section">
|
||||
<h2>{{ __("activitypub.moderation.blockedTitle") }}</h2>
|
||||
@@ -108,6 +140,7 @@
|
||||
document.addEventListener('alpine:init', () => {
|
||||
Alpine.data('moderationPage', () => ({
|
||||
newKeyword: '',
|
||||
newServerHostname: '',
|
||||
submitting: false,
|
||||
error: '',
|
||||
|
||||
@@ -157,6 +190,50 @@
|
||||
this.submitting = false;
|
||||
},
|
||||
|
||||
async addBlockedServer() {
|
||||
const hostname = this.newServerHostname.trim();
|
||||
if (!hostname) return;
|
||||
this.submitting = true;
|
||||
this.error = '';
|
||||
try {
|
||||
const res = await fetch(this.mountPath + '/admin/reader/block-server', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRF-Token': this.csrfToken,
|
||||
},
|
||||
body: JSON.stringify({ hostname }),
|
||||
});
|
||||
const data = await res.json();
|
||||
if (data.success) {
|
||||
const list = this.$refs.serverList;
|
||||
if (list) {
|
||||
const li = document.createElement('li');
|
||||
li.className = 'ap-moderation__entry';
|
||||
li.dataset.hostname = hostname;
|
||||
const code = document.createElement('code');
|
||||
code.textContent = hostname;
|
||||
const btn = document.createElement('button');
|
||||
btn.className = 'ap-moderation__remove';
|
||||
btn.textContent = 'Unblock';
|
||||
btn.addEventListener('click', () => {
|
||||
this.removeEntry(btn, 'unblock-server', { hostname });
|
||||
});
|
||||
li.append(code, btn);
|
||||
list.appendChild(li);
|
||||
}
|
||||
if (this.$refs.serverEmpty) this.$refs.serverEmpty.remove();
|
||||
this.newServerHostname = '';
|
||||
this.$refs.serverInput.focus();
|
||||
} else {
|
||||
this.error = data.error || 'Failed to block server';
|
||||
}
|
||||
} catch (e) {
|
||||
this.error = 'Request failed';
|
||||
}
|
||||
this.submitting = false;
|
||||
},
|
||||
|
||||
async removeEntry(el, action, payload) {
|
||||
const li = el.closest('li');
|
||||
if (!li) return;
|
||||
|
||||
@@ -65,6 +65,9 @@
|
||||
</time>
|
||||
{% if item.updated %}<span class="ap-card__edited" title="{{ item.updated | date('PPp') }}">✏️</span>{% endif %}
|
||||
</a>
|
||||
{% if item.visibility and item.visibility != "public" %}
|
||||
<span class="ap-card__visibility ap-card__visibility--{{ item.visibility }}" title="{% if item.visibility == 'unlisted' %}{{ __('activitypub.reader.compose.visibilityUnlisted') }}{% elif item.visibility == 'private' %}{{ __('activitypub.reader.compose.visibilityFollowers') }}{% elif item.visibility == 'direct' %}DM{% endif %}">{% if item.visibility == "unlisted" %}🔓{% elif item.visibility == "private" %}🔒{% elif item.visibility == "direct" %}✉️{% endif %}</span>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</header>
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
{% endif %}
|
||||
<span class="ap-notification__avatar ap-notification__avatar--default" aria-hidden="true">{{ item.actorName[0] | upper if item.actorName else "?" }}</span>
|
||||
<span class="ap-notification__type-badge">
|
||||
{% if item.type == "like" %}❤{% elif item.type == "boost" %}🔁{% elif item.type == "follow" %}👤{% elif item.type == "reply" %}💬{% elif item.type == "mention" %}{% if item.isDirect %}🔒{% else %}@{% endif %}{% elif item.type == "dm" %}✉{% elif item.type == "report" %}⚑{% endif %}
|
||||
{% if item.type == "like" %}❤{% elif item.type == "boost" %}🔁{% elif item.type == "follow" or item.type == "follow_request" %}👤{% elif item.type == "reply" %}💬{% elif item.type == "mention" %}{% if item.isDirect %}🔒{% else %}@{% endif %}{% elif item.type == "dm" %}✉{% elif item.type == "report" %}⚑{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -32,6 +32,8 @@
|
||||
{{ __("activitypub.notifications.boostedPost") }}
|
||||
{% elif item.type == "follow" %}
|
||||
{{ __("activitypub.notifications.followedYou") }}
|
||||
{% elif item.type == "follow_request" %}
|
||||
{{ __("activitypub.followRequest") }}
|
||||
{% elif item.type == "reply" %}
|
||||
{{ __("activitypub.notifications.repliedTo") }}
|
||||
{% elif item.type == "mention" %}
|
||||
|
||||
Reference in New Issue
Block a user