Merge branch 'follows' into 'develop'
Implement followers/following endpoints and account counters Closes #69 and #68 See merge request soapbox-pub/ditto!29
This commit is contained in:
commit
0c83e759f3
|
@ -18,6 +18,8 @@ import {
|
||||||
accountStatusesController,
|
accountStatusesController,
|
||||||
createAccountController,
|
createAccountController,
|
||||||
followController,
|
followController,
|
||||||
|
followersController,
|
||||||
|
followingController,
|
||||||
relationshipsController,
|
relationshipsController,
|
||||||
updateCredentialsController,
|
updateCredentialsController,
|
||||||
verifyCredentialsController,
|
verifyCredentialsController,
|
||||||
|
@ -105,6 +107,8 @@ app.get('/api/v1/accounts/search', accountSearchController);
|
||||||
app.get('/api/v1/accounts/lookup', accountLookupController);
|
app.get('/api/v1/accounts/lookup', accountLookupController);
|
||||||
app.get('/api/v1/accounts/relationships', relationshipsController);
|
app.get('/api/v1/accounts/relationships', relationshipsController);
|
||||||
app.post('/api/v1/accounts/:pubkey{[0-9a-f]{64}}/follow', followController);
|
app.post('/api/v1/accounts/:pubkey{[0-9a-f]{64}}/follow', followController);
|
||||||
|
app.get('/api/v1/accounts/:pubkey{[0-9a-f]{64}}/followers', followersController);
|
||||||
|
app.get('/api/v1/accounts/:pubkey{[0-9a-f]{64}}/following', followingController);
|
||||||
app.get('/api/v1/accounts/:pubkey{[0-9a-f]{64}}/statuses', accountStatusesController);
|
app.get('/api/v1/accounts/:pubkey{[0-9a-f]{64}}/statuses', accountStatusesController);
|
||||||
app.get('/api/v1/accounts/:pubkey{[0-9a-f]{64}}', accountController);
|
app.get('/api/v1/accounts/:pubkey{[0-9a-f]{64}}', accountController);
|
||||||
|
|
||||||
|
|
|
@ -1,13 +1,14 @@
|
||||||
import { type AppController } from '@/app.ts';
|
import { type AppController } from '@/app.ts';
|
||||||
import { type Filter, findReplyTag, z } from '@/deps.ts';
|
import { type Filter, findReplyTag, z } from '@/deps.ts';
|
||||||
import * as mixer from '@/mixer.ts';
|
import * as mixer from '@/mixer.ts';
|
||||||
import { getAuthor, getFollows, syncUser } from '@/queries.ts';
|
import { getAuthor, getFollowedPubkeys, getFollows, syncUser } from '@/queries.ts';
|
||||||
import { booleanParamSchema } from '@/schema.ts';
|
import { booleanParamSchema } from '@/schema.ts';
|
||||||
import { jsonMetaContentSchema } from '@/schemas/nostr.ts';
|
import { jsonMetaContentSchema } from '@/schemas/nostr.ts';
|
||||||
import { toAccount, toRelationship, toStatus } from '@/transformers/nostr-to-mastoapi.ts';
|
import { toAccount, toRelationship, toStatus } from '@/transformers/nostr-to-mastoapi.ts';
|
||||||
import { isFollowing, lookupAccount } from '@/utils.ts';
|
import { isFollowing, lookupAccount } from '@/utils.ts';
|
||||||
import { paginated, paginationSchema, parseBody } from '@/utils/web.ts';
|
import { paginated, paginationSchema, parseBody } from '@/utils/web.ts';
|
||||||
import { createEvent } from '@/utils/web.ts';
|
import { createEvent } from '@/utils/web.ts';
|
||||||
|
import { renderEventAccounts } from '@/views.ts';
|
||||||
|
|
||||||
const createAccountController: AppController = (c) => {
|
const createAccountController: AppController = (c) => {
|
||||||
return c.json({ error: 'Please log in with Nostr.' }, 405);
|
return c.json({ error: 'Please log in with Nostr.' }, 405);
|
||||||
|
@ -173,6 +174,25 @@ const followController: AppController = async (c) => {
|
||||||
return c.json(relationship);
|
return c.json(relationship);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const followersController: AppController = (c) => {
|
||||||
|
const pubkey = c.req.param('pubkey');
|
||||||
|
const params = paginationSchema.parse(c.req.query());
|
||||||
|
return renderEventAccounts(c, [{ kinds: [3], '#p': [pubkey], ...params }]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const followingController: AppController = async (c) => {
|
||||||
|
const pubkey = c.req.param('pubkey');
|
||||||
|
const pubkeys = await getFollowedPubkeys(pubkey);
|
||||||
|
|
||||||
|
// TODO: pagination by offset.
|
||||||
|
const accounts = await Promise.all(pubkeys.map(async (pubkey) => {
|
||||||
|
const event = await getAuthor(pubkey);
|
||||||
|
return event ? await toAccount(event) : undefined;
|
||||||
|
}));
|
||||||
|
|
||||||
|
return c.json(accounts.filter(Boolean));
|
||||||
|
};
|
||||||
|
|
||||||
export {
|
export {
|
||||||
accountController,
|
accountController,
|
||||||
accountLookupController,
|
accountLookupController,
|
||||||
|
@ -180,6 +200,8 @@ export {
|
||||||
accountStatusesController,
|
accountStatusesController,
|
||||||
createAccountController,
|
createAccountController,
|
||||||
followController,
|
followController,
|
||||||
|
followersController,
|
||||||
|
followingController,
|
||||||
relationshipsController,
|
relationshipsController,
|
||||||
updateCredentialsController,
|
updateCredentialsController,
|
||||||
verifyCredentialsController,
|
verifyCredentialsController,
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
import { type AppController } from '@/app.ts';
|
import { type AppController } from '@/app.ts';
|
||||||
import { type Event, ISO6391, z } from '@/deps.ts';
|
import { type Event, ISO6391, z } from '@/deps.ts';
|
||||||
import * as mixer from '@/mixer.ts';
|
import { getAncestors, getDescendants, getEvent } from '@/queries.ts';
|
||||||
import { getAncestors, getAuthor, getDescendants, getEvent } from '@/queries.ts';
|
import { toStatus } from '@/transformers/nostr-to-mastoapi.ts';
|
||||||
import { toAccount, toStatus } from '@/transformers/nostr-to-mastoapi.ts';
|
import { createEvent, paginationSchema, parseBody } from '@/utils/web.ts';
|
||||||
import { createEvent, paginated, parseBody } from '@/utils/web.ts';
|
import { renderEventAccounts } from '@/views.ts';
|
||||||
|
|
||||||
const createStatusSchema = z.object({
|
const createStatusSchema = z.object({
|
||||||
in_reply_to_id: z.string().regex(/[0-9a-f]{64}/).nullish(),
|
in_reply_to_id: z.string().regex(/[0-9a-f]{64}/).nullish(),
|
||||||
|
@ -129,34 +129,16 @@ const favouriteController: AppController = async (c) => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const favouritedByController: AppController = async (c) => {
|
const favouritedByController: AppController = (c) => {
|
||||||
const id = c.req.param('id');
|
const id = c.req.param('id');
|
||||||
|
const params = paginationSchema.parse(c.req.query());
|
||||||
const events = await mixer.getFilters([{ kinds: [7], '#e': [id] }]);
|
return renderEventAccounts(c, [{ kinds: [7], '#e': [id], ...params }]);
|
||||||
|
|
||||||
const accounts = await Promise.all(events.map(async ({ pubkey }) => {
|
|
||||||
const author = await getAuthor(pubkey);
|
|
||||||
if (author) {
|
|
||||||
return toAccount(author);
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
|
|
||||||
return paginated(c, events, accounts);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const rebloggedByController: AppController = async (c) => {
|
const rebloggedByController: AppController = (c) => {
|
||||||
const id = c.req.param('id');
|
const id = c.req.param('id');
|
||||||
|
const params = paginationSchema.parse(c.req.query());
|
||||||
const events = await mixer.getFilters([{ kinds: [6], '#e': [id] }]);
|
return renderEventAccounts(c, [{ kinds: [6], '#e': [id], ...params }]);
|
||||||
|
|
||||||
const accounts = await Promise.all(events.map(async ({ pubkey }) => {
|
|
||||||
const author = await getAuthor(pubkey);
|
|
||||||
if (author) {
|
|
||||||
return toAccount(author);
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
|
|
||||||
return paginated(c, events, accounts);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
|
|
@ -87,4 +87,14 @@ async function syncUser(pubkey: string): Promise<void> {
|
||||||
], { timeout: 5000 });
|
], { timeout: 5000 });
|
||||||
}
|
}
|
||||||
|
|
||||||
export { getAncestors, getAuthor, getDescendants, getEvent, getFeedPubkeys, getFollows, isLocallyFollowed, syncUser };
|
export {
|
||||||
|
getAncestors,
|
||||||
|
getAuthor,
|
||||||
|
getDescendants,
|
||||||
|
getEvent,
|
||||||
|
getFeedPubkeys,
|
||||||
|
getFollowedPubkeys,
|
||||||
|
getFollows,
|
||||||
|
isLocallyFollowed,
|
||||||
|
syncUser,
|
||||||
|
};
|
||||||
|
|
|
@ -5,7 +5,7 @@ import * as eventsDB from '@/db/events.ts';
|
||||||
import { type Event, findReplyTag, lodash, nip19, sanitizeHtml, TTLCache, unfurl, z } from '@/deps.ts';
|
import { type Event, findReplyTag, lodash, nip19, sanitizeHtml, TTLCache, unfurl, z } from '@/deps.ts';
|
||||||
import { verifyNip05Cached } from '@/nip05.ts';
|
import { verifyNip05Cached } from '@/nip05.ts';
|
||||||
import { getMediaLinks, type MediaLink, parseNoteContent } from '@/note.ts';
|
import { getMediaLinks, type MediaLink, parseNoteContent } from '@/note.ts';
|
||||||
import { getAuthor, getFollows } from '@/queries.ts';
|
import { getAuthor, getFollowedPubkeys, getFollows } from '@/queries.ts';
|
||||||
import { emojiTagSchema, filteredArray } from '@/schema.ts';
|
import { emojiTagSchema, filteredArray } from '@/schema.ts';
|
||||||
import { jsonMetaContentSchema } from '@/schemas/nostr.ts';
|
import { jsonMetaContentSchema } from '@/schemas/nostr.ts';
|
||||||
import { isFollowing, type Nip05, nostrDate, parseNip05, Time } from '@/utils.ts';
|
import { isFollowing, type Nip05, nostrDate, parseNip05, Time } from '@/utils.ts';
|
||||||
|
@ -24,14 +24,12 @@ async function toAccount(event: Event<0>, opts: ToAccountOpts = {}) {
|
||||||
const { name, nip05, picture, banner, about } = jsonMetaContentSchema.parse(event.content);
|
const { name, nip05, picture, banner, about } = jsonMetaContentSchema.parse(event.content);
|
||||||
const npub = nip19.npubEncode(pubkey);
|
const npub = nip19.npubEncode(pubkey);
|
||||||
|
|
||||||
let parsed05: Nip05 | undefined;
|
const [parsed05, followersCount, followingCount, statusesCount] = await Promise.all([
|
||||||
try {
|
parseAndVerifyNip05(nip05, pubkey),
|
||||||
if (nip05 && await verifyNip05Cached(nip05, pubkey)) {
|
eventsDB.countFilters([{ kinds: [3], '#p': [pubkey] }]),
|
||||||
parsed05 = parseNip05(nip05);
|
getFollowedPubkeys(pubkey).then((pubkeys) => pubkeys.length),
|
||||||
}
|
eventsDB.countFilters([{ kinds: [1], authors: [pubkey] }]),
|
||||||
} catch (_e) {
|
]);
|
||||||
//
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: pubkey,
|
id: pubkey,
|
||||||
|
@ -45,8 +43,8 @@ async function toAccount(event: Event<0>, opts: ToAccountOpts = {}) {
|
||||||
emojis: toEmojis(event),
|
emojis: toEmojis(event),
|
||||||
fields: [],
|
fields: [],
|
||||||
follow_requests_count: 0,
|
follow_requests_count: 0,
|
||||||
followers_count: 0,
|
followers_count: followersCount,
|
||||||
following_count: 0,
|
following_count: followingCount,
|
||||||
fqn: parsed05?.handle || npub,
|
fqn: parsed05?.handle || npub,
|
||||||
header: banner || DEFAULT_BANNER,
|
header: banner || DEFAULT_BANNER,
|
||||||
header_static: banner || DEFAULT_BANNER,
|
header_static: banner || DEFAULT_BANNER,
|
||||||
|
@ -64,12 +62,18 @@ async function toAccount(event: Event<0>, opts: ToAccountOpts = {}) {
|
||||||
follow_requests_count: 0,
|
follow_requests_count: 0,
|
||||||
}
|
}
|
||||||
: undefined,
|
: undefined,
|
||||||
statuses_count: 0,
|
statuses_count: statusesCount,
|
||||||
url: Conf.local(`/users/${pubkey}`),
|
url: Conf.local(`/users/${pubkey}`),
|
||||||
username: parsed05?.nickname || npub.substring(0, 8),
|
username: parsed05?.nickname || npub.substring(0, 8),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function parseAndVerifyNip05(nip05: string | undefined, pubkey: string): Promise<Nip05 | undefined> {
|
||||||
|
if (nip05 && await verifyNip05Cached(nip05, pubkey)) {
|
||||||
|
return parseNip05(nip05);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function toMention(pubkey: string) {
|
async function toMention(pubkey: string) {
|
||||||
const profile = await getAuthor(pubkey);
|
const profile = await getAuthor(pubkey);
|
||||||
const account = profile ? await toAccount(profile) : undefined;
|
const account = profile ? await toAccount(profile) : undefined;
|
||||||
|
|
|
@ -0,0 +1,27 @@
|
||||||
|
import { AppContext } from '@/app.ts';
|
||||||
|
import { type Filter } from '@/deps.ts';
|
||||||
|
import * as mixer from '@/mixer.ts';
|
||||||
|
import { getAuthor } from '@/queries.ts';
|
||||||
|
import { toAccount } from '@/transformers/nostr-to-mastoapi.ts';
|
||||||
|
import { paginated } from '@/utils/web.ts';
|
||||||
|
|
||||||
|
/** Render account objects for the author of each event. */
|
||||||
|
async function renderEventAccounts(c: AppContext, filters: Filter[]) {
|
||||||
|
const events = await mixer.getFilters(filters);
|
||||||
|
const pubkeys = new Set(events.map(({ pubkey }) => pubkey));
|
||||||
|
|
||||||
|
if (!pubkeys.size) {
|
||||||
|
return c.json([]);
|
||||||
|
}
|
||||||
|
|
||||||
|
const accounts = await Promise.all([...pubkeys].map(async (pubkey) => {
|
||||||
|
const author = await getAuthor(pubkey);
|
||||||
|
if (author) {
|
||||||
|
return toAccount(author);
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
return paginated(c, events, accounts);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { renderEventAccounts };
|
Loading…
Reference in New Issue