@rmdes/indiekit-endpoint-activitypub 2.0.17 → 2.0.19
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 +100 -92
- package/lib/controllers/post-detail.js +57 -25
- package/package.json +1 -1
package/index.js
CHANGED
|
@@ -875,107 +875,115 @@ export default class ActivityPubEndpoint {
|
|
|
875
875
|
_publicationUrl: this._publicationUrl,
|
|
876
876
|
};
|
|
877
877
|
|
|
878
|
-
//
|
|
879
|
-
|
|
880
|
-
|
|
878
|
+
// Create indexes — wrapped in try-catch because collection references
|
|
879
|
+
// may be undefined if MongoDB hasn't finished connecting yet.
|
|
880
|
+
// Indexes are idempotent; they'll be created on next successful startup.
|
|
881
|
+
try {
|
|
882
|
+
// TTL index for activity cleanup (MongoDB handles expiry automatically)
|
|
883
|
+
const retentionDays = this.options.activityRetentionDays;
|
|
884
|
+
if (retentionDays > 0) {
|
|
885
|
+
this._collections.ap_activities.createIndex(
|
|
886
|
+
{ receivedAt: 1 },
|
|
887
|
+
{ expireAfterSeconds: retentionDays * 86_400 },
|
|
888
|
+
);
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
// Performance indexes for inbox handlers and batch refollow
|
|
892
|
+
this._collections.ap_followers.createIndex(
|
|
893
|
+
{ actorUrl: 1 },
|
|
894
|
+
{ unique: true, background: true },
|
|
895
|
+
);
|
|
896
|
+
this._collections.ap_following.createIndex(
|
|
897
|
+
{ actorUrl: 1 },
|
|
898
|
+
{ unique: true, background: true },
|
|
899
|
+
);
|
|
900
|
+
this._collections.ap_following.createIndex(
|
|
901
|
+
{ source: 1 },
|
|
902
|
+
{ background: true },
|
|
903
|
+
);
|
|
881
904
|
this._collections.ap_activities.createIndex(
|
|
882
|
-
{
|
|
883
|
-
{
|
|
905
|
+
{ objectUrl: 1 },
|
|
906
|
+
{ background: true },
|
|
907
|
+
);
|
|
908
|
+
this._collections.ap_activities.createIndex(
|
|
909
|
+
{ type: 1, actorUrl: 1, objectUrl: 1 },
|
|
910
|
+
{ background: true },
|
|
884
911
|
);
|
|
885
|
-
}
|
|
886
|
-
|
|
887
|
-
// Performance indexes for inbox handlers and batch refollow
|
|
888
|
-
this._collections.ap_followers.createIndex(
|
|
889
|
-
{ actorUrl: 1 },
|
|
890
|
-
{ unique: true, background: true },
|
|
891
|
-
);
|
|
892
|
-
this._collections.ap_following.createIndex(
|
|
893
|
-
{ actorUrl: 1 },
|
|
894
|
-
{ unique: true, background: true },
|
|
895
|
-
);
|
|
896
|
-
this._collections.ap_following.createIndex(
|
|
897
|
-
{ source: 1 },
|
|
898
|
-
{ background: true },
|
|
899
|
-
);
|
|
900
|
-
this._collections.ap_activities.createIndex(
|
|
901
|
-
{ objectUrl: 1 },
|
|
902
|
-
{ background: true },
|
|
903
|
-
);
|
|
904
|
-
this._collections.ap_activities.createIndex(
|
|
905
|
-
{ type: 1, actorUrl: 1, objectUrl: 1 },
|
|
906
|
-
{ background: true },
|
|
907
|
-
);
|
|
908
|
-
|
|
909
|
-
// Reader indexes (timeline, notifications, moderation, interactions)
|
|
910
|
-
this._collections.ap_timeline.createIndex(
|
|
911
|
-
{ uid: 1 },
|
|
912
|
-
{ unique: true, background: true },
|
|
913
|
-
);
|
|
914
|
-
this._collections.ap_timeline.createIndex(
|
|
915
|
-
{ published: -1 },
|
|
916
|
-
{ background: true },
|
|
917
|
-
);
|
|
918
|
-
this._collections.ap_timeline.createIndex(
|
|
919
|
-
{ "author.url": 1 },
|
|
920
|
-
{ background: true },
|
|
921
|
-
);
|
|
922
|
-
this._collections.ap_timeline.createIndex(
|
|
923
|
-
{ type: 1, published: -1 },
|
|
924
|
-
{ background: true },
|
|
925
|
-
);
|
|
926
912
|
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
913
|
+
// Reader indexes (timeline, notifications, moderation, interactions)
|
|
914
|
+
this._collections.ap_timeline.createIndex(
|
|
915
|
+
{ uid: 1 },
|
|
916
|
+
{ unique: true, background: true },
|
|
917
|
+
);
|
|
918
|
+
this._collections.ap_timeline.createIndex(
|
|
919
|
+
{ published: -1 },
|
|
920
|
+
{ background: true },
|
|
921
|
+
);
|
|
922
|
+
this._collections.ap_timeline.createIndex(
|
|
923
|
+
{ "author.url": 1 },
|
|
924
|
+
{ background: true },
|
|
925
|
+
);
|
|
926
|
+
this._collections.ap_timeline.createIndex(
|
|
927
|
+
{ type: 1, published: -1 },
|
|
928
|
+
{ background: true },
|
|
929
|
+
);
|
|
943
930
|
|
|
944
|
-
// TTL index for notification cleanup
|
|
945
|
-
const notifRetention = this.options.notificationRetentionDays;
|
|
946
|
-
if (notifRetention > 0) {
|
|
947
931
|
this._collections.ap_notifications.createIndex(
|
|
948
|
-
{
|
|
949
|
-
{
|
|
932
|
+
{ uid: 1 },
|
|
933
|
+
{ unique: true, background: true },
|
|
934
|
+
);
|
|
935
|
+
this._collections.ap_notifications.createIndex(
|
|
936
|
+
{ published: -1 },
|
|
937
|
+
{ background: true },
|
|
938
|
+
);
|
|
939
|
+
this._collections.ap_notifications.createIndex(
|
|
940
|
+
{ read: 1 },
|
|
941
|
+
{ background: true },
|
|
942
|
+
);
|
|
943
|
+
this._collections.ap_notifications.createIndex(
|
|
944
|
+
{ type: 1, published: -1 },
|
|
945
|
+
{ background: true },
|
|
950
946
|
);
|
|
951
|
-
}
|
|
952
947
|
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
this._collections.ap_muted.createIndex(
|
|
962
|
-
{ keyword: 1 },
|
|
963
|
-
{ unique: true, sparse: true, background: true },
|
|
964
|
-
);
|
|
948
|
+
// TTL index for notification cleanup
|
|
949
|
+
const notifRetention = this.options.notificationRetentionDays;
|
|
950
|
+
if (notifRetention > 0) {
|
|
951
|
+
this._collections.ap_notifications.createIndex(
|
|
952
|
+
{ createdAt: 1 },
|
|
953
|
+
{ expireAfterSeconds: notifRetention * 86_400 },
|
|
954
|
+
);
|
|
955
|
+
}
|
|
965
956
|
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
957
|
+
// Drop non-sparse indexes if they exist (created by earlier versions),
|
|
958
|
+
// then recreate with sparse:true so multiple null values are allowed.
|
|
959
|
+
this._collections.ap_muted.dropIndex("url_1").catch(() => {});
|
|
960
|
+
this._collections.ap_muted.dropIndex("keyword_1").catch(() => {});
|
|
961
|
+
this._collections.ap_muted.createIndex(
|
|
962
|
+
{ url: 1 },
|
|
963
|
+
{ unique: true, sparse: true, background: true },
|
|
964
|
+
);
|
|
965
|
+
this._collections.ap_muted.createIndex(
|
|
966
|
+
{ keyword: 1 },
|
|
967
|
+
{ unique: true, sparse: true, background: true },
|
|
968
|
+
);
|
|
970
969
|
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
970
|
+
this._collections.ap_blocked.createIndex(
|
|
971
|
+
{ url: 1 },
|
|
972
|
+
{ unique: true, background: true },
|
|
973
|
+
);
|
|
974
|
+
|
|
975
|
+
this._collections.ap_interactions.createIndex(
|
|
976
|
+
{ objectUrl: 1, type: 1 },
|
|
977
|
+
{ unique: true, background: true },
|
|
978
|
+
);
|
|
979
|
+
this._collections.ap_interactions.createIndex(
|
|
980
|
+
{ type: 1 },
|
|
981
|
+
{ background: true },
|
|
982
|
+
);
|
|
983
|
+
} catch {
|
|
984
|
+
// Index creation failed — collections not yet available.
|
|
985
|
+
// Indexes already exist from previous startups; non-fatal.
|
|
986
|
+
}
|
|
979
987
|
|
|
980
988
|
// Seed actor profile from config on first run
|
|
981
989
|
this._seedProfile().catch((error) => {
|
|
@@ -153,7 +153,15 @@ export function postDetailController(mountPath, plugin) {
|
|
|
153
153
|
|
|
154
154
|
let object = null;
|
|
155
155
|
|
|
156
|
-
|
|
156
|
+
// If stored item has no media, re-fetch from Fedify to pick up
|
|
157
|
+
// attachments that were missed before the async iteration fix.
|
|
158
|
+
const storedHasNoMedia =
|
|
159
|
+
timelineItem &&
|
|
160
|
+
(!timelineItem.photo || timelineItem.photo.length === 0) &&
|
|
161
|
+
(!timelineItem.video || timelineItem.video.length === 0) &&
|
|
162
|
+
(!timelineItem.audio || timelineItem.audio.length === 0);
|
|
163
|
+
|
|
164
|
+
if (!timelineItem || storedHasNoMedia) {
|
|
157
165
|
// Not in local timeline — fetch via lookupObject
|
|
158
166
|
const handle = plugin.options.actor.handle;
|
|
159
167
|
const ctx = plugin._federation.createContext(
|
|
@@ -185,7 +193,8 @@ export function postDetailController(mountPath, plugin) {
|
|
|
185
193
|
}
|
|
186
194
|
}
|
|
187
195
|
|
|
188
|
-
if (!object) {
|
|
196
|
+
if (!object && !storedHasNoMedia) {
|
|
197
|
+
// Truly not found (no local item either)
|
|
189
198
|
return response.status(404).render("activitypub-post-detail", {
|
|
190
199
|
title: response.locals.__("activitypub.reader.post.title"),
|
|
191
200
|
notFound: true, objectUrl, mountPath,
|
|
@@ -194,34 +203,57 @@ export function postDetailController(mountPath, plugin) {
|
|
|
194
203
|
});
|
|
195
204
|
}
|
|
196
205
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
206
|
+
if (object) {
|
|
207
|
+
// If it's an actor (Person, Service, Application), redirect to profile
|
|
208
|
+
if (
|
|
209
|
+
object instanceof Person ||
|
|
210
|
+
object instanceof Service ||
|
|
211
|
+
object instanceof Application
|
|
212
|
+
) {
|
|
213
|
+
return response.redirect(
|
|
214
|
+
`${mountPath}/admin/reader/profile?url=${encodeURIComponent(objectUrl)}`,
|
|
215
|
+
);
|
|
216
|
+
}
|
|
207
217
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
218
|
+
// Extract timeline item data from the Fedify object
|
|
219
|
+
if (object instanceof Note || object instanceof Article) {
|
|
220
|
+
try {
|
|
221
|
+
const freshItem = await extractObjectData(object);
|
|
222
|
+
|
|
223
|
+
// If re-fetch found media that the stored item was missing, update MongoDB
|
|
224
|
+
if (storedHasNoMedia && timelineCol) {
|
|
225
|
+
const hasMedia =
|
|
226
|
+
(freshItem.photo && freshItem.photo.length > 0) ||
|
|
227
|
+
(freshItem.video && freshItem.video.length > 0) ||
|
|
228
|
+
(freshItem.audio && freshItem.audio.length > 0);
|
|
229
|
+
if (hasMedia) {
|
|
230
|
+
await timelineCol.updateOne(
|
|
231
|
+
{ $or: [{ uid: objectUrl }, { url: objectUrl }] },
|
|
232
|
+
{ $set: { photo: freshItem.photo, video: freshItem.video, audio: freshItem.audio } },
|
|
233
|
+
).catch(() => {});
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
timelineItem = freshItem;
|
|
238
|
+
} catch (error) {
|
|
239
|
+
// If re-extraction fails but we have a stored item, use it
|
|
240
|
+
if (!storedHasNoMedia) {
|
|
241
|
+
console.error(`[post-detail] extractObjectData failed for ${objectUrl}:`, error.message);
|
|
242
|
+
return response.status(500).render("error", {
|
|
243
|
+
title: "Error",
|
|
244
|
+
content: "Failed to extract post data",
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
// storedHasNoMedia=true means timelineItem still has the stored data
|
|
248
|
+
}
|
|
249
|
+
} else if (!storedHasNoMedia) {
|
|
250
|
+
return response.status(400).render("error", {
|
|
215
251
|
title: "Error",
|
|
216
|
-
content: "
|
|
252
|
+
content: "Object is not a viewable post (must be Note or Article)",
|
|
217
253
|
});
|
|
218
254
|
}
|
|
219
|
-
} else {
|
|
220
|
-
return response.status(400).render("error", {
|
|
221
|
-
title: "Error",
|
|
222
|
-
content: "Object is not a viewable post (must be Note or Article)",
|
|
223
|
-
});
|
|
224
255
|
}
|
|
256
|
+
// If object is null and storedHasNoMedia, we fall through with the stored timelineItem
|
|
225
257
|
}
|
|
226
258
|
|
|
227
259
|
// Build interaction state for this post
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rmdes/indiekit-endpoint-activitypub",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.19",
|
|
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",
|