From da0139ff4e4870d035ca799e364e0c7e424e17d7 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sun, 2 Jun 2024 22:46:07 -0500 Subject: [PATCH 1/4] Suggestions: add offset based pagination --- src/controllers/api/suggestions.ts | 78 +++++++++++++++++++----------- src/utils/api.ts | 12 +++-- 2 files changed, 60 insertions(+), 30 deletions(-) diff --git a/src/controllers/api/suggestions.ts b/src/controllers/api/suggestions.ts index 012244a1..620de684 100644 --- a/src/controllers/api/suggestions.ts +++ b/src/controllers/api/suggestions.ts @@ -1,51 +1,75 @@ -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 { hydrateEvents } from '@/storages/hydrate.ts'; +import { listPaginationSchema, paginatedList, PaginatedListParams } from '@/utils/api.ts'; import { getTagSet } from '@/utils/tags.ts'; import { accountFromPubkey, renderAccount } from '@/views/mastodon/accounts.ts'; export const suggestionsV1Controller: AppController = async (c) => { - const store = c.get('store'); const signal = c.req.raw.signal; - const accounts = await renderSuggestedAccounts(store, signal); - - return c.json(accounts); + const params = listPaginationSchema.parse(c.req.query()); + const suggestions = await renderV2Suggestions(c, params, signal); + const accounts = suggestions.map(({ account }) => account); + return paginatedList(c, params, accounts); }; export const suggestionsV2Controller: AppController = async (c) => { - const store = c.get('store'); const signal = c.req.raw.signal; - const accounts = await renderSuggestedAccounts(store, signal); - - const suggestions = accounts.map((account) => ({ - source: 'staff', - account, - })); - - return c.json(suggestions); + const params = listPaginationSchema.parse(c.req.query()); + const suggestions = await renderV2Suggestions(c, params, signal); + return paginatedList(c, params, suggestions); }; -async function renderSuggestedAccounts(store: NStore, signal?: AbortSignal) { - const [follows] = await store.query( - [{ kinds: [3], authors: [Conf.pubkey], limit: 1 }], - { signal }, - ); +async function renderV2Suggestions(c: AppContext, params: PaginatedListParams, signal?: AbortSignal) { + const { offset, limit } = params; - // TODO: pagination - const pubkeys = [...getTagSet(follows?.tags ?? [], 'p')].slice(0, 20); + const store = c.get('store'); + const signer = c.get('signer'); + const pubkey = await signer?.getPublicKey(); + + const filters: NostrFilter[] = [ + { kinds: [3], 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] = [ + 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, + ]; + + const [suggested, follows, mutes] = [ + getTagSet(suggestedEvent?.tags ?? [], 'p'), + getTagSet(followsEvent?.tags ?? [], 'p'), + getTagSet(mutesEvent?.tags ?? [], 'p'), + ]; + + const ignored = follows.union(mutes); + const pubkeys = suggested.difference(ignored); + + const authors = [...pubkeys].slice(offset, offset + limit); const profiles = await store.query( - [{ kinds: [0], authors: pubkeys, limit: pubkeys.length }], + [{ kinds: [0], authors, limit: authors.length }], { signal }, ) .then((events) => hydrateEvents({ events, store, signal })); - const accounts = await Promise.all(pubkeys.map((pubkey) => { + return Promise.all(authors.map((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 ? renderAccount(profile) : accountFromPubkey(pubkey), + }; + })); } diff --git a/src/utils/api.ts b/src/utils/api.ts index 5ab4cc61..3cc8b7d2 100644 --- a/src/utils/api.ts +++ b/src/utils/api.ts @@ -202,11 +202,16 @@ function buildListLinkHeader(url: string, params: { offset: number; limit: numbe return `<${next}>; rel="next", <${prev}>; rel="prev"`; } +interface PaginatedListParams { + offset: number; + limit: number; +} + /** paginate a list of tags. */ function paginatedList( c: AppContext, - params: { offset: number; limit: number }, - entities: (Entity | undefined)[], + params: PaginatedListParams, + entities: unknown[], headers: HeaderRecord = {}, ) { const link = buildListLinkHeader(c.req.url, params); @@ -217,7 +222,7 @@ function paginatedList( } // 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); } @@ -255,6 +260,7 @@ export { localRequest, paginated, paginatedList, + type PaginatedListParams, type PaginationParams, paginationSchema, parseBody, From 68375c555e3a3600065ae3fd273e60f987e421e8 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sun, 2 Jun 2024 22:52:05 -0500 Subject: [PATCH 2/4] Add trending users to suggested --- src/controllers/api/suggestions.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/controllers/api/suggestions.ts b/src/controllers/api/suggestions.ts index 620de684..cda20481 100644 --- a/src/controllers/api/suggestions.ts +++ b/src/controllers/api/suggestions.ts @@ -32,6 +32,7 @@ async function renderV2Suggestions(c: AppContext, params: PaginatedListParams, s 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) { @@ -41,20 +42,24 @@ async function renderV2Suggestions(c: AppContext, params: PaginatedListParams, s const events = await store.query(filters, { signal }); - const [suggestedEvent, followsEvent, mutesEvent] = [ + 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, follows, mutes] = [ + 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.difference(ignored); + const pubkeys = suggested.union(trending).difference(ignored); const authors = [...pubkeys].slice(offset, offset + limit); From a325908d41934d65553a66d1bfdc724d50e9e2ca Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sun, 2 Jun 2024 22:54:38 -0500 Subject: [PATCH 3/4] Fix rendering suggested accounts --- src/controllers/api/suggestions.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/controllers/api/suggestions.ts b/src/controllers/api/suggestions.ts index cda20481..30151576 100644 --- a/src/controllers/api/suggestions.ts +++ b/src/controllers/api/suggestions.ts @@ -69,12 +69,12 @@ async function renderV2Suggestions(c: AppContext, params: PaginatedListParams, s ) .then((events) => hydrateEvents({ events, store, signal })); - return Promise.all(authors.map((pubkey) => { + return Promise.all(authors.map(async (pubkey) => { const profile = profiles.find((event) => event.pubkey === pubkey); return { source: suggested.has(pubkey) ? 'staff' : 'global', - account: profile ? renderAccount(profile) : accountFromPubkey(pubkey), + account: profile ? await renderAccount(profile) : await accountFromPubkey(pubkey), }; })); } From 6d5dbc029e742ff8b06889581e0d2b04ddfefa19 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sun, 2 Jun 2024 22:59:06 -0500 Subject: [PATCH 4/4] Remove self from suggested list --- src/controllers/api/suggestions.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/controllers/api/suggestions.ts b/src/controllers/api/suggestions.ts index 30151576..b56851a2 100644 --- a/src/controllers/api/suggestions.ts +++ b/src/controllers/api/suggestions.ts @@ -61,6 +61,10 @@ async function renderV2Suggestions(c: AppContext, params: PaginatedListParams, s 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(