Merge branch 'suggestions-global' into 'main'
Suggestions global See merge request soapbox-pub/ditto!351
This commit is contained in:
commit
9cdfc188aa
|
@ -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),
|
||||||
|
};
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
Loading…
Reference in New Issue