+12
__generated__/index.ts
+12
__generated__/index.ts
···
15
15
import * as SocialGrainGalleryGetGallery from './types/social/grain/gallery/getGallery.ts'
16
16
import * as SocialGrainFeedGetTimeline from './types/social/grain/feed/getTimeline.ts'
17
17
import * as SocialGrainActorGetProfile from './types/social/grain/actor/getProfile.ts'
18
+
import * as SocialGrainActorSearchActors from './types/social/grain/actor/searchActors.ts'
18
19
19
20
export const APP_BSKY_GRAPH = {
20
21
DefsModlist: 'app.bsky.graph.defs#modlist',
···
315
316
>,
316
317
) {
317
318
const nsid = 'social.grain.actor.getProfile' // @ts-ignore
319
+
return this._server.xrpc.method(nsid, cfg)
320
+
}
321
+
322
+
searchActors<AV extends AuthVerifier>(
323
+
cfg: ConfigOf<
324
+
AV,
325
+
SocialGrainActorSearchActors.Handler<ExtractAuth<AV>>,
326
+
SocialGrainActorSearchActors.HandlerReqCtx<ExtractAuth<AV>>
327
+
>,
328
+
) {
329
+
const nsid = 'social.grain.actor.searchActors' // @ts-ignore
318
330
return this._server.xrpc.method(nsid, cfg)
319
331
}
320
332
}
+50
__generated__/lexicons.ts
+50
__generated__/lexicons.ts
···
3337
3337
},
3338
3338
},
3339
3339
},
3340
+
SocialGrainActorSearchActors: {
3341
+
lexicon: 1,
3342
+
id: 'social.grain.actor.searchActors',
3343
+
defs: {
3344
+
main: {
3345
+
type: 'query',
3346
+
description:
3347
+
'Find actors (profiles) matching search criteria. Does not require auth.',
3348
+
parameters: {
3349
+
type: 'params',
3350
+
properties: {
3351
+
q: {
3352
+
type: 'string',
3353
+
description:
3354
+
'Search query string. Syntax, phrase, boolean, and faceting is unspecified, but Lucene query syntax is recommended.',
3355
+
},
3356
+
limit: {
3357
+
type: 'integer',
3358
+
minimum: 1,
3359
+
maximum: 100,
3360
+
default: 25,
3361
+
},
3362
+
cursor: {
3363
+
type: 'string',
3364
+
},
3365
+
},
3366
+
},
3367
+
output: {
3368
+
encoding: 'application/json',
3369
+
schema: {
3370
+
type: 'object',
3371
+
required: ['actors'],
3372
+
properties: {
3373
+
cursor: {
3374
+
type: 'string',
3375
+
},
3376
+
actors: {
3377
+
type: 'array',
3378
+
items: {
3379
+
type: 'ref',
3380
+
ref: 'lex:social.grain.actor.defs#profileView',
3381
+
},
3382
+
},
3383
+
},
3384
+
},
3385
+
},
3386
+
},
3387
+
},
3388
+
},
3340
3389
SocialGrainActorProfile: {
3341
3390
lexicon: 1,
3342
3391
id: 'social.grain.actor.profile',
···
3885
3934
SocialGrainFavorite: 'social.grain.favorite',
3886
3935
SocialGrainActorDefs: 'social.grain.actor.defs',
3887
3936
SocialGrainActorGetProfile: 'social.grain.actor.getProfile',
3937
+
SocialGrainActorSearchActors: 'social.grain.actor.searchActors',
3888
3938
SocialGrainActorProfile: 'social.grain.actor.profile',
3889
3939
SocialGrainPhotoDefs: 'social.grain.photo.defs',
3890
3940
SocialGrainPhotoExif: 'social.grain.photo.exif',
+40
-1
src/api/mod.ts
+40
-1
src/api/mod.ts
···
1
+
import { ProfileView } from "$lexicon/types/social/grain/actor/defs.ts";
1
2
import {
2
3
OutputSchema as GetProfileOutputSchema,
3
4
QueryParams as GetProfileQueryParams,
4
5
} from "$lexicon/types/social/grain/actor/getProfile.ts";
6
+
import {
7
+
OutputSchema as SearchActorsOutputSchema,
8
+
QueryParams as SearchActorsQueryParams,
9
+
} from "$lexicon/types/social/grain/actor/searchActors.ts";
5
10
import {
6
11
OutputSchema as GetTimelineOutputSchema,
7
12
} from "$lexicon/types/social/grain/feed/getTimeline.ts";
···
22
27
} from "$lexicon/types/social/grain/notification/getNotifications.ts";
23
28
import { AtUri } from "@atproto/syntax";
24
29
import { BffMiddleware, OAUTH_ROUTES, route } from "@bigmoves/bff";
25
-
import { getActorGalleries, getActorProfileDetailed } from "../lib/actor.ts";
30
+
import {
31
+
getActorGalleries,
32
+
getActorProfileDetailed,
33
+
searchActors,
34
+
} from "../lib/actor.ts";
26
35
import { BadRequestError } from "../lib/errors.ts";
27
36
import { getGallery } from "../lib/gallery.ts";
28
37
import { getNotifications } from "../lib/notifications.ts";
···
117
126
);
118
127
},
119
128
),
129
+
route(
130
+
"/xrpc/social.grain.actor.searchActors",
131
+
(req, _params, ctx) => {
132
+
const url = new URL(req.url);
133
+
const { q } = searchActorsQueryParams(url);
134
+
let results: ProfileView[] = [];
135
+
if (!q) {
136
+
results = [];
137
+
} else {
138
+
results = searchActors(
139
+
q,
140
+
ctx,
141
+
);
142
+
}
143
+
return ctx.json(
144
+
{ actors: results } as SearchActorsOutputSchema,
145
+
);
146
+
},
147
+
),
120
148
];
121
149
122
150
function getProfileQueryParams(url: URL): GetProfileQueryParams {
···
146
174
const uri = url.searchParams.get("uri");
147
175
if (!uri) throw new BadRequestError("Missing uri parameter");
148
176
return { uri };
177
+
}
178
+
179
+
function searchActorsQueryParams(url: URL): SearchActorsQueryParams {
180
+
const q = url.searchParams.get("q");
181
+
if (!q) throw new BadRequestError("Missing q parameter");
182
+
const limit = parseInt(url.searchParams.get("limit") ?? "50", 10);
183
+
if (isNaN(limit) || limit <= 0) {
184
+
throw new BadRequestError("Invalid limit parameter");
185
+
}
186
+
const cursor = url.searchParams.get("cursor") ?? undefined;
187
+
return { q, limit, cursor };
149
188
}
150
189
151
190
// function getNotificationsQueryParams(url: URL): GetNotificationsQueryParams {
+57
src/lib/actor.ts
+57
src/lib/actor.ts
···
322
322
return profileToView(profile, handle);
323
323
});
324
324
}
325
+
326
+
export function searchActors(query: string, ctx: BffContext) {
327
+
const actors = ctx.indexService.searchActors(query);
328
+
329
+
const { items } = ctx.indexService.getRecords<WithBffMeta<GrainProfile>>(
330
+
"social.grain.actor.profile",
331
+
{
332
+
where: {
333
+
OR: [
334
+
...(actors.length > 0
335
+
? [{
336
+
field: "did",
337
+
in: actors.map((actor) => actor.did),
338
+
}]
339
+
: []),
340
+
{
341
+
field: "displayName",
342
+
contains: query,
343
+
},
344
+
{
345
+
field: "did",
346
+
contains: query,
347
+
},
348
+
],
349
+
},
350
+
},
351
+
);
352
+
353
+
const profileMap = new Map<string, WithBffMeta<GrainProfile>>();
354
+
for (const item of items) {
355
+
profileMap.set(item.did, item);
356
+
}
357
+
358
+
const actorMap = new Map();
359
+
actors.forEach((actor) => {
360
+
actorMap.set(actor.did, actor);
361
+
});
362
+
363
+
const profileViews = [];
364
+
365
+
for (const actor of actors) {
366
+
if (profileMap.has(actor.did)) {
367
+
const profile = profileMap.get(actor.did)!;
368
+
profileViews.push(profileToView(profile, actor.handle));
369
+
}
370
+
}
371
+
372
+
for (const profile of items) {
373
+
if (!actorMap.has(profile.did)) {
374
+
const handle = ctx.indexService.getActor(profile.did)?.handle;
375
+
if (!handle) continue;
376
+
profileViews.push(profileToView(profile, handle));
377
+
}
378
+
}
379
+
380
+
return profileViews;
381
+
}
+3
-61
src/routes/explore.tsx
+3
-61
src/routes/explore.tsx
···
1
1
import { ProfileView } from "$lexicon/types/social/grain/actor/defs.ts";
2
-
import { Record as Profile } from "$lexicon/types/social/grain/actor/profile.ts";
3
2
import { Un$Typed } from "$lexicon/util.ts";
4
-
import { BffContext, RouteHandler, WithBffMeta } from "@bigmoves/bff";
3
+
import { BffContext, RouteHandler } from "@bigmoves/bff";
5
4
import { ComponentChildren } from "preact";
6
5
import { ActorAvatar } from "../components/ActorAvatar.tsx";
7
6
import { Input } from "../components/Input.tsx";
8
7
import { LabelerAvatar } from "../components/LabelerAvatar.tsx";
9
-
import { profileToView } from "../lib/actor.ts";
8
+
import { searchActors } from "../lib/actor.ts";
10
9
import { getPageMeta } from "../meta.ts";
11
10
import type { State } from "../state.ts";
12
11
···
20
19
const query = url.searchParams.get("q") ?? "";
21
20
ctx.state.meta = [{ title: "Explore — Grain" }, ...getPageMeta("/explore")];
22
21
if (query) {
23
-
const profileViews = doSearch(query, ctx);
22
+
const profileViews = searchActors(query, ctx);
24
23
if (req.headers.get("hx-request")) {
25
24
if (profileViews.length === 0) {
26
25
return ctx.html(<p>No results for "{query}"</p>);
···
104
103
</>
105
104
);
106
105
}
107
-
108
-
function doSearch(query: string, ctx: BffContext<State>) {
109
-
const actors = ctx.indexService.searchActors(query);
110
-
111
-
const { items } = ctx.indexService.getRecords<WithBffMeta<Profile>>(
112
-
"social.grain.actor.profile",
113
-
{
114
-
where: {
115
-
OR: [
116
-
...(actors.length > 0
117
-
? [{
118
-
field: "did",
119
-
in: actors.map((actor) => actor.did),
120
-
}]
121
-
: []),
122
-
{
123
-
field: "displayName",
124
-
contains: query,
125
-
},
126
-
{
127
-
field: "did",
128
-
contains: query,
129
-
},
130
-
],
131
-
},
132
-
},
133
-
);
134
-
135
-
const profileMap = new Map<string, WithBffMeta<Profile>>();
136
-
for (const item of items) {
137
-
profileMap.set(item.did, item);
138
-
}
139
-
140
-
const actorMap = new Map();
141
-
actors.forEach((actor) => {
142
-
actorMap.set(actor.did, actor);
143
-
});
144
-
145
-
const profileViews = [];
146
-
147
-
for (const actor of actors) {
148
-
if (profileMap.has(actor.did)) {
149
-
const profile = profileMap.get(actor.did)!;
150
-
profileViews.push(profileToView(profile, actor.handle));
151
-
}
152
-
}
153
-
154
-
for (const profile of items) {
155
-
if (!actorMap.has(profile.did)) {
156
-
const handle = ctx.indexService.getActor(profile.did)?.handle;
157
-
if (!handle) continue;
158
-
profileViews.push(profileToView(profile, handle));
159
-
}
160
-
}
161
-
162
-
return profileViews;
163
-
}