@rmdes/indiekit-endpoint-activitypub 3.6.3 → 3.6.5
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.
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve a remote account via WebFinger + ActivityPub actor fetch.
|
|
3
|
+
* Uses the Fedify federation instance to perform discovery.
|
|
4
|
+
*
|
|
5
|
+
* Shared by accounts.js (lookup) and search.js (resolve=true).
|
|
6
|
+
*/
|
|
7
|
+
import { serializeAccount } from "../entities/account.js";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @param {string} acct - Account identifier (user@domain or URL)
|
|
11
|
+
* @param {object} pluginOptions - Plugin options with federation, handle, publicationUrl
|
|
12
|
+
* @param {string} baseUrl - Server base URL
|
|
13
|
+
* @returns {Promise<object|null>} Serialized Mastodon Account or null
|
|
14
|
+
*/
|
|
15
|
+
export async function resolveRemoteAccount(acct, pluginOptions, baseUrl) {
|
|
16
|
+
const { federation, handle, publicationUrl } = pluginOptions;
|
|
17
|
+
if (!federation) return null;
|
|
18
|
+
|
|
19
|
+
try {
|
|
20
|
+
const ctx = federation.createContext(
|
|
21
|
+
new URL(publicationUrl),
|
|
22
|
+
{ handle, publicationUrl },
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
// Determine lookup URI
|
|
26
|
+
let actorUri;
|
|
27
|
+
if (acct.includes("@")) {
|
|
28
|
+
const parts = acct.replace(/^@/, "").split("@");
|
|
29
|
+
const username = parts[0];
|
|
30
|
+
const domain = parts[1];
|
|
31
|
+
if (!username || !domain) return null;
|
|
32
|
+
actorUri = `acct:${username}@${domain}`;
|
|
33
|
+
} else if (acct.startsWith("http")) {
|
|
34
|
+
actorUri = acct;
|
|
35
|
+
} else {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const actor = await ctx.lookupObject(actorUri);
|
|
40
|
+
if (!actor) return null;
|
|
41
|
+
|
|
42
|
+
// Extract data from the Fedify actor object
|
|
43
|
+
const name = actor.name?.toString() || actor.preferredUsername?.toString() || "";
|
|
44
|
+
const actorUrl = actor.id?.href || "";
|
|
45
|
+
const username = actor.preferredUsername?.toString() || "";
|
|
46
|
+
const domain = actorUrl ? new URL(actorUrl).hostname : "";
|
|
47
|
+
const summary = actor.summary?.toString() || "";
|
|
48
|
+
|
|
49
|
+
// Get avatar
|
|
50
|
+
let avatarUrl = "";
|
|
51
|
+
try {
|
|
52
|
+
const icon = await actor.getIcon();
|
|
53
|
+
avatarUrl = icon?.url?.href || "";
|
|
54
|
+
} catch { /* ignore */ }
|
|
55
|
+
|
|
56
|
+
// Get header image
|
|
57
|
+
let headerUrl = "";
|
|
58
|
+
try {
|
|
59
|
+
const image = await actor.getImage();
|
|
60
|
+
headerUrl = image?.url?.href || "";
|
|
61
|
+
} catch { /* ignore */ }
|
|
62
|
+
|
|
63
|
+
// Get collection counts (followers, following, outbox)
|
|
64
|
+
let followersCount = 0;
|
|
65
|
+
let followingCount = 0;
|
|
66
|
+
let statusesCount = 0;
|
|
67
|
+
try {
|
|
68
|
+
const followers = await actor.getFollowers();
|
|
69
|
+
if (followers?.totalItems != null) followersCount = followers.totalItems;
|
|
70
|
+
} catch { /* ignore */ }
|
|
71
|
+
try {
|
|
72
|
+
const following = await actor.getFollowing();
|
|
73
|
+
if (following?.totalItems != null) followingCount = following.totalItems;
|
|
74
|
+
} catch { /* ignore */ }
|
|
75
|
+
try {
|
|
76
|
+
const outbox = await actor.getOutbox();
|
|
77
|
+
if (outbox?.totalItems != null) statusesCount = outbox.totalItems;
|
|
78
|
+
} catch { /* ignore */ }
|
|
79
|
+
|
|
80
|
+
// Get published/created date
|
|
81
|
+
const published = actor.published
|
|
82
|
+
? String(actor.published)
|
|
83
|
+
: null;
|
|
84
|
+
|
|
85
|
+
// Profile fields from attachments
|
|
86
|
+
const fields = [];
|
|
87
|
+
try {
|
|
88
|
+
for await (const attachment of actor.getAttachments()) {
|
|
89
|
+
if (attachment?.name) {
|
|
90
|
+
fields.push({
|
|
91
|
+
name: attachment.name?.toString() || "",
|
|
92
|
+
value: attachment.value?.toString() || "",
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
} catch { /* ignore */ }
|
|
97
|
+
|
|
98
|
+
const account = serializeAccount(
|
|
99
|
+
{
|
|
100
|
+
name,
|
|
101
|
+
url: actorUrl,
|
|
102
|
+
photo: avatarUrl,
|
|
103
|
+
handle: `@${username}@${domain}`,
|
|
104
|
+
summary,
|
|
105
|
+
image: headerUrl,
|
|
106
|
+
bot: actor.constructor?.name === "Service" || actor.constructor?.name === "Application",
|
|
107
|
+
attachments: fields.length > 0 ? fields : undefined,
|
|
108
|
+
createdAt: published || undefined,
|
|
109
|
+
},
|
|
110
|
+
{ baseUrl },
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
// Override counts with real data from AP collections
|
|
114
|
+
account.followers_count = followersCount;
|
|
115
|
+
account.following_count = followingCount;
|
|
116
|
+
account.statuses_count = statusesCount;
|
|
117
|
+
|
|
118
|
+
return account;
|
|
119
|
+
} catch (error) {
|
|
120
|
+
console.warn(`[Mastodon API] Remote account resolution failed for ${acct}:`, error.message);
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
@@ -9,6 +9,7 @@ import { serializeCredentialAccount, serializeAccount } from "../entities/accoun
|
|
|
9
9
|
import { serializeStatus } from "../entities/status.js";
|
|
10
10
|
import { accountId, remoteActorId } from "../helpers/id-mapping.js";
|
|
11
11
|
import { buildPaginationQuery, parseLimit, setPaginationHeaders } from "../helpers/pagination.js";
|
|
12
|
+
import { resolveRemoteAccount } from "../helpers/resolve-account.js";
|
|
12
13
|
|
|
13
14
|
const router = express.Router(); // eslint-disable-line new-cap
|
|
14
15
|
|
|
@@ -100,7 +101,7 @@ router.get("/api/v1/accounts/lookup", async (req, res, next) => {
|
|
|
100
101
|
}
|
|
101
102
|
}
|
|
102
103
|
|
|
103
|
-
// Check followers
|
|
104
|
+
// Check followers for known remote actors
|
|
104
105
|
const follower = await collections.ap_followers.findOne({
|
|
105
106
|
$or: [
|
|
106
107
|
{ handle: `@${bareAcct}` },
|
|
@@ -116,6 +117,38 @@ router.get("/api/v1/accounts/lookup", async (req, res, next) => {
|
|
|
116
117
|
);
|
|
117
118
|
}
|
|
118
119
|
|
|
120
|
+
// Check following
|
|
121
|
+
const following = await collections.ap_following.findOne({
|
|
122
|
+
$or: [
|
|
123
|
+
{ handle: `@${bareAcct}` },
|
|
124
|
+
{ handle: bareAcct },
|
|
125
|
+
],
|
|
126
|
+
});
|
|
127
|
+
if (following) {
|
|
128
|
+
return res.json(
|
|
129
|
+
serializeAccount(
|
|
130
|
+
{ name: following.name, url: following.actorUrl, photo: following.avatar, handle: following.handle },
|
|
131
|
+
{ baseUrl },
|
|
132
|
+
),
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Check timeline authors (people whose posts are in our timeline)
|
|
137
|
+
const timelineAuthor = await collections.ap_timeline.findOne({
|
|
138
|
+
"author.handle": { $in: [`@${bareAcct}`, bareAcct] },
|
|
139
|
+
});
|
|
140
|
+
if (timelineAuthor?.author) {
|
|
141
|
+
return res.json(
|
|
142
|
+
serializeAccount(timelineAuthor.author, { baseUrl }),
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Resolve remotely via federation (WebFinger + actor fetch)
|
|
147
|
+
const resolved = await resolveRemoteAccount(bareAcct, pluginOptions, baseUrl);
|
|
148
|
+
if (resolved) {
|
|
149
|
+
return res.json(resolved);
|
|
150
|
+
}
|
|
151
|
+
|
|
119
152
|
return res.status(404).json({ error: "Record not found" });
|
|
120
153
|
} catch (error) {
|
|
121
154
|
next(error);
|
|
@@ -7,6 +7,7 @@ import express from "express";
|
|
|
7
7
|
import { serializeStatus } from "../entities/status.js";
|
|
8
8
|
import { serializeAccount } from "../entities/account.js";
|
|
9
9
|
import { parseLimit } from "../helpers/pagination.js";
|
|
10
|
+
import { resolveRemoteAccount } from "../helpers/resolve-account.js";
|
|
10
11
|
|
|
11
12
|
const router = express.Router(); // eslint-disable-line new-cap
|
|
12
13
|
|
|
@@ -21,6 +22,9 @@ router.get("/api/v2/search", async (req, res, next) => {
|
|
|
21
22
|
const limit = parseLimit(req.query.limit);
|
|
22
23
|
const offset = Math.max(0, Number.parseInt(req.query.offset, 10) || 0);
|
|
23
24
|
|
|
25
|
+
const resolve = req.query.resolve === "true";
|
|
26
|
+
const pluginOptions = req.app.locals.mastodonPluginOptions || {};
|
|
27
|
+
|
|
24
28
|
if (!query) {
|
|
25
29
|
return res.json({ accounts: [], statuses: [], hashtags: [] });
|
|
26
30
|
}
|
|
@@ -75,6 +79,14 @@ router.get("/api/v2/search", async (req, res, next) => {
|
|
|
75
79
|
}
|
|
76
80
|
if (results.accounts.length >= limit) break;
|
|
77
81
|
}
|
|
82
|
+
|
|
83
|
+
// If no local results and resolve=true, try remote lookup
|
|
84
|
+
if (results.accounts.length === 0 && resolve && query.includes("@")) {
|
|
85
|
+
const resolved = await resolveRemoteAccount(query, pluginOptions, baseUrl);
|
|
86
|
+
if (resolved) {
|
|
87
|
+
results.accounts.push(resolved);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
78
90
|
}
|
|
79
91
|
|
|
80
92
|
// ─── Status search ───────────────────────────────────────────────────
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rmdes/indiekit-endpoint-activitypub",
|
|
3
|
-
"version": "3.6.
|
|
3
|
+
"version": "3.6.5",
|
|
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",
|