Merge branch 'suggestions-global' into 'main'

Suggestions global

See merge request soapbox-pub/ditto!351
This commit is contained in:
Alex Gleason 2024-06-03 04:03:48 +00:00
commit 9cdfc188aa
2 changed files with 69 additions and 30 deletions

View File

@ -1,51 +1,84 @@
import { NStore } from '@nostrify/nostrify'; import { NostrFilter } from '@nostrify/nostrify';
import { matchFilter } from 'nostr-tools';
import { AppController } from '@/app.ts'; import { AppContext, AppController } from '@/app.ts';
import { Conf } from '@/config.ts'; import { Conf } from '@/config.ts';
import { hydrateEvents } from '@/storages/hydrate.ts'; import { hydrateEvents } from '@/storages/hydrate.ts';
import { listPaginationSchema, paginatedList, PaginatedListParams } from '@/utils/api.ts';
import { getTagSet } from '@/utils/tags.ts'; import { getTagSet } from '@/utils/tags.ts';
import { accountFromPubkey, renderAccount } from '@/views/mastodon/accounts.ts'; import { accountFromPubkey, renderAccount } from '@/views/mastodon/accounts.ts';
export const suggestionsV1Controller: AppController = async (c) => { export const suggestionsV1Controller: AppController = async (c) => {
const store = c.get('store');
const signal = c.req.raw.signal; const signal = c.req.raw.signal;
const accounts = await renderSuggestedAccounts(store, signal); const params = listPaginationSchema.parse(c.req.query());
const suggestions = await renderV2Suggestions(c, params, signal);
return c.json(accounts); const accounts = suggestions.map(({ account }) => account);
return paginatedList(c, params, accounts);
}; };
export const suggestionsV2Controller: AppController = async (c) => { export const suggestionsV2Controller: AppController = async (c) => {
const store = c.get('store');
const signal = c.req.raw.signal; const signal = c.req.raw.signal;
const accounts = await renderSuggestedAccounts(store, signal); const params = listPaginationSchema.parse(c.req.query());
const suggestions = await renderV2Suggestions(c, params, signal);
const suggestions = accounts.map((account) => ({ return paginatedList(c, params, suggestions);
source: 'staff',
account,
}));
return c.json(suggestions);
}; };
async function renderSuggestedAccounts(store: NStore, signal?: AbortSignal) { async function renderV2Suggestions(c: AppContext, params: PaginatedListParams, signal?: AbortSignal) {
const [follows] = await store.query( const { offset, limit } = params;
[{ kinds: [3], authors: [Conf.pubkey], limit: 1 }],
{ signal },
);
// TODO: pagination const store = c.get('store');
const pubkeys = [...getTagSet(follows?.tags ?? [], 'p')].slice(0, 20); const signer = c.get('signer');
const pubkey = await signer?.getPublicKey();
const filters: NostrFilter[] = [
{ kinds: [3], authors: [Conf.pubkey], limit: 1 },
{ kinds: [1985], '#L': ['pub.ditto.trends'], '#l': [`#p`], authors: [Conf.pubkey], limit: 1 },
];
if (pubkey) {
filters.push({ kinds: [3], authors: [pubkey], limit: 1 });
filters.push({ kinds: [10000], authors: [pubkey], limit: 1 });
}
const events = await store.query(filters, { signal });
const [suggestedEvent, followsEvent, mutesEvent, trendingEvent] = [
events.find((event) => matchFilter({ kinds: [3], authors: [Conf.pubkey] }, event)),
pubkey ? events.find((event) => matchFilter({ kinds: [3], authors: [pubkey] }, event)) : undefined,
pubkey ? events.find((event) => matchFilter({ kinds: [10000], authors: [pubkey] }, event)) : undefined,
events.find((event) =>
matchFilter({ kinds: [1985], '#L': ['pub.ditto.trends'], '#l': [`#p`], authors: [Conf.pubkey], limit: 1 }, event)
),
];
const [suggested, trending, follows, mutes] = [
getTagSet(suggestedEvent?.tags ?? [], 'p'),
getTagSet(trendingEvent?.tags ?? [], 'p'),
getTagSet(followsEvent?.tags ?? [], 'p'),
getTagSet(mutesEvent?.tags ?? [], 'p'),
];
const ignored = follows.union(mutes);
const pubkeys = suggested.union(trending).difference(ignored);
if (pubkey) {
pubkeys.delete(pubkey);
}
const authors = [...pubkeys].slice(offset, offset + limit);
const profiles = await store.query( const profiles = await store.query(
[{ kinds: [0], authors: pubkeys, limit: pubkeys.length }], [{ kinds: [0], authors, limit: authors.length }],
{ signal }, { signal },
) )
.then((events) => hydrateEvents({ events, store, signal })); .then((events) => hydrateEvents({ events, store, signal }));
const accounts = await Promise.all(pubkeys.map((pubkey) => { return Promise.all(authors.map(async (pubkey) => {
const profile = profiles.find((event) => event.pubkey === pubkey); const profile = profiles.find((event) => event.pubkey === pubkey);
return profile ? renderAccount(profile) : accountFromPubkey(pubkey);
}));
return accounts.filter(Boolean); return {
source: suggested.has(pubkey) ? 'staff' : 'global',
account: profile ? await renderAccount(profile) : await accountFromPubkey(pubkey),
};
}));
} }

View File

@ -202,11 +202,16 @@ function buildListLinkHeader(url: string, params: { offset: number; limit: numbe
return `<${next}>; rel="next", <${prev}>; rel="prev"`; return `<${next}>; rel="next", <${prev}>; rel="prev"`;
} }
interface PaginatedListParams {
offset: number;
limit: number;
}
/** paginate a list of tags. */ /** paginate a list of tags. */
function paginatedList( function paginatedList(
c: AppContext, c: AppContext,
params: { offset: number; limit: number }, params: PaginatedListParams,
entities: (Entity | undefined)[], entities: unknown[],
headers: HeaderRecord = {}, headers: HeaderRecord = {},
) { ) {
const link = buildListLinkHeader(c.req.url, params); const link = buildListLinkHeader(c.req.url, params);
@ -217,7 +222,7 @@ function paginatedList(
} }
// Filter out undefined entities. // Filter out undefined entities.
const results = entities.filter((entity): entity is Entity => Boolean(entity)); const results = entities.filter(Boolean);
return c.json(results, 200, headers); return c.json(results, 200, headers);
} }
@ -255,6 +260,7 @@ export {
localRequest, localRequest,
paginated, paginated,
paginatedList, paginatedList,
type PaginatedListParams,
type PaginationParams, type PaginationParams,
paginationSchema, paginationSchema,
parseBody, parseBody,