Feat add actors to meilisearch (#1048)
* initial commit * update api docs * adds search token versioning * make id filterable * isLocal -> is_local * add iri to actor index * update gitignore * Merge remote-tracking branch 'origin/main' into feat-add-actors-to-meilisearch * fix sharing for newly discovered actors * fix ActorSearchResult type --------- Co-authored-by: Christian Beutel <>
This commit is contained in:
@@ -12,6 +12,7 @@ import type { Actor } from '$lib/models/activitypub/actor'
|
||||
import { normalizeLocale } from '$lib/i18n/locales'
|
||||
import { handleError } from '$lib/util/api_util'
|
||||
|
||||
const SEARCH_TOKEN_VERSION = 1;
|
||||
|
||||
function csrf(allowedPaths: string[]): Handle {
|
||||
return async ({ event, resolve }) => {
|
||||
@@ -85,12 +86,12 @@ const auth: Handle = async ({ event, resolve }) => {
|
||||
const currentUserId = pb.authStore.record?.id || 'public';
|
||||
|
||||
if (meiliCookie) {
|
||||
const [token, ownerId] = meiliCookie.split('|');
|
||||
const [token, ownerId, version] = meiliCookie.split('|');
|
||||
|
||||
if (ownerId === currentUserId) {
|
||||
if (ownerId === currentUserId && Number(version) === SEARCH_TOKEN_VERSION) {
|
||||
meilisearchToken = token;
|
||||
} else {
|
||||
// Identity mismatch (e.g. just logged in/out)
|
||||
// Identity mismatch (e.g. just logged in/out) or stale token version
|
||||
event.cookies.delete('meilisearch_token', { path: '/' });
|
||||
}
|
||||
}
|
||||
@@ -99,7 +100,7 @@ const auth: Handle = async ({ event, resolve }) => {
|
||||
try {
|
||||
const tokenResponse = await pb.send("/search/token", { method: "GET", fetch: event.fetch });
|
||||
meilisearchToken = tokenResponse.token
|
||||
event.cookies.set('meilisearch_token', `${meilisearchToken}|${currentUserId}`, {
|
||||
event.cookies.set('meilisearch_token', `${meilisearchToken}|${currentUserId}|${SEARCH_TOKEN_VERSION}`, {
|
||||
path: '/',
|
||||
httpOnly: false,
|
||||
maxAge: 60 * 60 * 24,
|
||||
@@ -143,7 +144,7 @@ const auth: Handle = async ({ event, resolve }) => {
|
||||
|
||||
if (pb.authStore.record) {
|
||||
settings = await pb.collection('settings').getFirstListItem<Settings>(`user="${pb.authStore.record.id}"`, { requestKey: null })
|
||||
actor = await pb.collection("activitypub_actors").getFirstListItem(`isLocal=1&&user='${pb.authStore.record.id}'`)
|
||||
actor = await pb.collection("activitypub_actors").getFirstListItem(`is_local=1&&user='${pb.authStore.record.id}'`)
|
||||
}
|
||||
const meiliHost = env.MEILI_URL;
|
||||
if (!meiliHost) {
|
||||
@@ -190,4 +191,4 @@ const removeLinkFromHeaders: Handle =
|
||||
}
|
||||
|
||||
|
||||
export const handle = sequence(csrf(['/api/v1']), auth, removeLinkFromHeaders)
|
||||
export const handle = sequence(csrf(['/api/v1']), auth, removeLinkFromHeaders)
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import type { Actor } from "$lib/models/activitypub/actor";
|
||||
import type { Actor, ActorSearchResult } from "$lib/models/activitypub/actor";
|
||||
import { searchActors } from "$lib/stores/search_store";
|
||||
import { show_toast } from "$lib/stores/toast_store.svelte";
|
||||
import { _ } from "svelte-i18n";
|
||||
@@ -32,10 +32,10 @@
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const actors: Actor[] = await searchActors(q, includeSelf);
|
||||
const actors: ActorSearchResult[] = await searchActors(q, includeSelf);
|
||||
searchItems = actors.map((a) => ({
|
||||
text: a.username,
|
||||
description: `@${a.preferred_username}${a.isLocal ? "" : "@" + a.domain}`,
|
||||
description: `@${a.preferred_username}${a.is_local ? "" : "@" + a.domain}`,
|
||||
value: a,
|
||||
icon: "user",
|
||||
}));
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
import Toggle from "./toggle.svelte";
|
||||
import type { SearchItem } from "./search.svelte";
|
||||
import type { SuggestionProps } from "@tiptap/suggestion";
|
||||
import type { Actor } from "$lib/models/activitypub/actor";
|
||||
import type { Actor, ActorSearchResult } from "$lib/models/activitypub/actor";
|
||||
import { searchActors } from "$lib/stores/search_store";
|
||||
import { show_toast } from "$lib/stores/toast_store.svelte";
|
||||
|
||||
@@ -103,13 +103,13 @@
|
||||
allowToIncludeChar: true,
|
||||
items: async ({ query }) => {
|
||||
try {
|
||||
const actors: Actor[] = await searchActors(
|
||||
const actors: ActorSearchResult[] = await searchActors(
|
||||
query,
|
||||
true,
|
||||
);
|
||||
return actors.map((a) => ({
|
||||
text: a.username!,
|
||||
description: `@${a.preferred_username}${a.isLocal ? "" : "@" + a.domain}`,
|
||||
description: `@${a.preferred_username}${a.is_local ? "" : "@" + a.domain}`,
|
||||
value: a,
|
||||
icon:
|
||||
a.icon ||
|
||||
@@ -155,7 +155,7 @@
|
||||
return (_: Event, item: SearchItem) => {
|
||||
props.command({
|
||||
id: item.value.iri,
|
||||
label: `${item.value.preferred_username}${item.value.isLocal ? "" : "@" + item.value.domain}`,
|
||||
label: `${item.value.preferred_username}${item.value.is_local ? "" : "@" + item.value.domain}`,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
@@ -84,7 +84,7 @@
|
||||
}
|
||||
|
||||
async function shareList(item: SelectItem) {
|
||||
if (!item.value.isLocal && !list.public) {
|
||||
if (!item.value.is_local && !list.public) {
|
||||
displayShareError = true;
|
||||
return;
|
||||
}
|
||||
@@ -162,7 +162,7 @@
|
||||
alt="avatar"
|
||||
/>
|
||||
<p>
|
||||
{`@${share.expand.actor.preferred_username}${share.expand.actor.isLocal ? "" : "@" + share.expand.actor.domain}`}
|
||||
{`@${share.expand.actor.preferred_username}${share.expand.actor.is_local ? "" : "@" + share.expand.actor.domain}`}
|
||||
</p>
|
||||
<span
|
||||
class="basis-full text-sm text-center text-gray-500"
|
||||
@@ -170,13 +170,13 @@
|
||||
>
|
||||
<div
|
||||
class="shrink-0"
|
||||
class:tooltip={!share.expand.actor.isLocal}
|
||||
class:tooltip={!share.expand.actor.is_local}
|
||||
data-title={$_("remote-users-cannot-edit")}
|
||||
>
|
||||
<Select
|
||||
bind:value={share.permission}
|
||||
items={permissionSelectItems}
|
||||
disabled={!share.expand.actor.isLocal}
|
||||
disabled={!share.expand.actor.is_local}
|
||||
onchange={(value) =>
|
||||
updateSharePermission(share, value)}
|
||||
></Select>
|
||||
|
||||
@@ -55,7 +55,7 @@
|
||||
alt="avatar"
|
||||
/>
|
||||
<p class="font-semibold text-base">
|
||||
{`@${share.expand.actor.username}${share.expand.actor.isLocal ? "" : "@" + share.expand.actor.domain}`}
|
||||
{`@${share.expand.actor.username}${share.expand.actor.is_local ? "" : "@" + share.expand.actor.domain}`}
|
||||
</p>
|
||||
<span class="mx-2 text-sm text-gray-500"
|
||||
>{$_("can")}</span
|
||||
|
||||
@@ -190,13 +190,13 @@
|
||||
<p
|
||||
class="tooltip flex justify-center"
|
||||
data-title="@{log.expand.author.preferred_username}{log.expand.author
|
||||
.isLocal
|
||||
.is_local
|
||||
? ''
|
||||
: '@' + log.expand.author.domain}"
|
||||
>
|
||||
<a
|
||||
href="/profile/@{log.expand.author.preferred_username?.toLowerCase()}{log
|
||||
.expand.author.isLocal
|
||||
.expand.author.is_local
|
||||
? ''
|
||||
: '@' + log.expand.author.domain}"
|
||||
>
|
||||
|
||||
@@ -173,7 +173,7 @@
|
||||
alt="avatar"
|
||||
/>
|
||||
{trail.expand.author.preferred_username}{trail.expand.author
|
||||
.isLocal
|
||||
.is_local
|
||||
? ""
|
||||
: "@" + trail.expand.author.domain}
|
||||
</p>
|
||||
|
||||
@@ -86,7 +86,7 @@
|
||||
return;
|
||||
}
|
||||
|
||||
if (!item.value.isLocal && !trail.public) {
|
||||
if (!item.value.is_local && !trail.public) {
|
||||
displayShareError = true;
|
||||
return;
|
||||
}
|
||||
@@ -167,11 +167,11 @@
|
||||
<img
|
||||
class="rounded-full w-8 aspect-square mr-2"
|
||||
src={share.expand.actor.icon ||
|
||||
`https://api.dicebear.com/7.x/initials/svg?seed=${share.expand.actor.username}&backgroundType=gradientLinear`}
|
||||
`https://api.dicebear.com/7.x/initials/svg?seed=${share.expand.actor.preferred_username}&backgroundType=gradientLinear`}
|
||||
alt="avatar"
|
||||
/>
|
||||
<p>
|
||||
{`@${share.expand.actor.username}${share.expand.actor.isLocal ? "" : "@" + share.expand.actor.domain}`}
|
||||
{`@${share.expand.actor.username}${share.expand.actor.is_local ? "" : "@" + share.expand.actor.domain}`}
|
||||
</p>
|
||||
<span
|
||||
class="basis-full text-sm text-gray-500 text-end"
|
||||
@@ -179,13 +179,13 @@
|
||||
>
|
||||
<div
|
||||
class="shrink-0"
|
||||
class:tooltip={!share.expand.actor.isLocal}
|
||||
class:tooltip={!share.expand.actor.is_local}
|
||||
data-title={$_("remote-users-cannot-edit")}
|
||||
>
|
||||
<Select
|
||||
value={share.permission}
|
||||
items={permissionSelectItems}
|
||||
disabled={!share.expand.actor.isLocal}
|
||||
disabled={!share.expand.actor.is_local}
|
||||
onchange={(value) =>
|
||||
updateSharePermission(share, value)}
|
||||
></Select>
|
||||
|
||||
@@ -176,7 +176,7 @@
|
||||
{#if trail.expand && trail.expand.author}
|
||||
<div class="author-icon">
|
||||
<img
|
||||
title={`${trail.public ? $_("public") + " " : ""}${$_("by")} @${trail.expand.author.preferred_username}${trail.expand.author.isLocal ? "" : "@" + trail.expand.author.domain}`}
|
||||
title={`${trail.public ? $_("public") + " " : ""}${$_("by")} @${trail.expand.author.preferred_username}${trail.expand.author.is_local ? "" : "@" + trail.expand.author.domain}`}
|
||||
class="rounded-full w-5 aspect-square mx-1 inline"
|
||||
src={trail.expand.author.icon ||
|
||||
`https://api.dicebear.com/7.x/initials/svg?seed=${trail.expand.author.preferred_username}&backgroundType=gradientLinear`}
|
||||
|
||||
@@ -15,11 +15,21 @@ export interface Actor {
|
||||
icon?: string;
|
||||
followers?: string;
|
||||
following?: string;
|
||||
isLocal: boolean;
|
||||
is_local: boolean;
|
||||
public_key: string;
|
||||
last_fetched: string;
|
||||
user?: string
|
||||
expand?: {
|
||||
user?: User
|
||||
}
|
||||
}
|
||||
|
||||
export interface ActorSearchResult {
|
||||
id: string;
|
||||
username: string;
|
||||
preferred_username: string;
|
||||
domain: string;
|
||||
is_local: boolean;
|
||||
iri: string;
|
||||
icon?: string;
|
||||
}
|
||||
@@ -1,8 +1,7 @@
|
||||
import type { Actor } from "$lib/models/activitypub/actor";
|
||||
import type { ActorSearchResult } from "$lib/models/activitypub/actor";
|
||||
import { defaultTrailSearchAttributes, type TrailSearchResult } from "$lib/models/trail";
|
||||
import { APIError } from "$lib/util/api_util";
|
||||
import type { Hits, MultiSearchParams, MultiSearchResponse, MultiSearchResult, SearchParams, SearchResponse } from "meilisearch";
|
||||
import type { ListResult } from "pocketbase";
|
||||
|
||||
export type LocationSearchResult = {
|
||||
name: string;
|
||||
@@ -262,16 +261,16 @@ export async function searchMulti(options: MultiSearchParams): Promise<MultiSear
|
||||
return response.results
|
||||
}
|
||||
|
||||
export async function searchActors(q: string, includeSelf: boolean = true): Promise<Actor[]> {
|
||||
export async function searchActors(q: string, includeSelf: boolean = true): Promise<ActorSearchResult[]> {
|
||||
try {
|
||||
const r = await fetch(`/api/v1/search/actor?q=${q}&includeSelf=${includeSelf}`,)
|
||||
|
||||
if (!r.ok) {
|
||||
return []
|
||||
}
|
||||
const response: ListResult<Actor> = await r.json()
|
||||
const response: SearchResponse<ActorSearchResult> = await r.json()
|
||||
|
||||
return response.items
|
||||
return response.hits
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
|
||||
|
||||
@@ -725,7 +725,7 @@ export async function searchResultToTrailList(hits: Hits<TrailSearchResult>): Pr
|
||||
expand: {
|
||||
author: {
|
||||
collectionId: "activitypub_actors",
|
||||
isLocal: (h.domain?.length ?? 0) == 0,
|
||||
is_local: (h.domain?.length ?? 0) == 0,
|
||||
id: h.author,
|
||||
icon: h.author_avatar,
|
||||
preferred_username: h.author_name,
|
||||
|
||||
@@ -17,6 +17,18 @@ export function splitUsername(handle: string, localDomain?: string) {
|
||||
return [user, domain]
|
||||
}
|
||||
|
||||
export function isValidPubHandle(handle: string): boolean {
|
||||
// Regex breakdown:
|
||||
// ^@? - Optional leading '@'
|
||||
// [a-zA-Z0-9_.-]+ - Username: alphanumeric, underscores, dots, hyphens (minimum 1 char)
|
||||
// @ - Literal '@' separator
|
||||
// ([a-zA-Z0-9-]+\.)+ - Domain segments (e.g., "mastodon.")
|
||||
// [a-zA-Z]{2,}$ - TLD (e.g., "social", "com" - minimum 2 chars)
|
||||
const activityPubRegex = /^@?[a-zA-Z0-9_.-]+@([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}$/;
|
||||
|
||||
return activityPubRegex.test(handle);
|
||||
}
|
||||
|
||||
export function isRemoteHandle(handle: string, origin: string) {
|
||||
const [, domain] = splitUsername(handle, origin);
|
||||
if (!domain) {
|
||||
|
||||
@@ -62,7 +62,7 @@
|
||||
const actors = await searchActors(q);
|
||||
searchDropdownItems = actors.map((a) => ({
|
||||
text: a.username,
|
||||
description: `@${a.preferred_username}${a.isLocal ? "" : "@" + a.domain}`,
|
||||
description: `@${a.preferred_username}${a.is_local ? "" : "@" + a.domain}`,
|
||||
value: a,
|
||||
icon:
|
||||
a.icon ||
|
||||
@@ -119,7 +119,7 @@
|
||||
goto(`/lists/${item.value}`);
|
||||
} else if (item.value.preferred_username) {
|
||||
goto(
|
||||
`/profile/@${item.value.preferred_username}${item.value.isLocal ? "" : "@" + item.value.domain}`,
|
||||
`/profile/@${item.value.preferred_username}${item.value.is_local ? "" : "@" + item.value.domain}`,
|
||||
);
|
||||
} else {
|
||||
goto(`/map/?lat=${item.value.lat}&lon=${item.value.lon}`);
|
||||
|
||||
@@ -46,7 +46,7 @@ export async function GET(event: RequestEvent) {
|
||||
|
||||
const [username, domain] = splitUsername(fullUsername, env.ORIGIN)
|
||||
|
||||
const actor: Actor = await event.locals.pb.collection("activitypub_actors").getFirstListItem(`preferred_username:lower='${username?.toLowerCase()}'&&isLocal=1`)
|
||||
const actor: Actor = await event.locals.pb.collection("activitypub_actors").getFirstListItem(`preferred_username:lower='${username?.toLowerCase()}'&&is_local=1`)
|
||||
const user: UserAnonymous = await event.locals.pb.collection("users_anonymous").getOne(actor.user!)
|
||||
|
||||
|
||||
|
||||
@@ -56,7 +56,7 @@ export async function GET(event: RequestEvent) {
|
||||
|
||||
const [username, domain] = splitUsername(fullUsername, env.ORIGIN)
|
||||
|
||||
const actor: Actor = await event.locals.pb.collection("activitypub_actors").getFirstListItem(`preferred_username:lower='${username?.toLowerCase()}'&&isLocal=true`)
|
||||
const actor: Actor = await event.locals.pb.collection("activitypub_actors").getFirstListItem(`preferred_username:lower='${username?.toLowerCase()}'&&is_local=true`)
|
||||
|
||||
const followers: ListResult<Follow> = await event.locals.pb.collection("follows").getList(intPage, 10, { sort: "-created", filter: `followee='${actor.id}'&&status='accepted'`, expand: "follower" })
|
||||
|
||||
|
||||
@@ -56,7 +56,7 @@ export async function GET(event: RequestEvent) {
|
||||
|
||||
const [username, domain] = splitUsername(fullUsername, env.ORIGIN)
|
||||
|
||||
const actor: Actor = await event.locals.pb.collection("activitypub_actors").getFirstListItem(`preferred_username:lower='${username?.toLowerCase()}'&&isLocal=true`)
|
||||
const actor: Actor = await event.locals.pb.collection("activitypub_actors").getFirstListItem(`preferred_username:lower='${username?.toLowerCase()}'&&is_local=true`)
|
||||
|
||||
const followers: ListResult<Follow> = await event.locals.pb.collection("follows").getList(intPage, 10, { sort: "-created", filter: `follower='${actor.id}'&&status='accepted'`, expand: "followee" })
|
||||
|
||||
|
||||
@@ -62,7 +62,7 @@ export async function GET(event: RequestEvent) {
|
||||
const [username, domain] = splitUsername(fullUsername, env.ORIGIN)
|
||||
|
||||
|
||||
const actor: Actor = await event.locals.pb.collection("activitypub_actors").getFirstListItem(`preferred_username:lower='${username?.toLowerCase()}'&&isLocal=true`)
|
||||
const actor: Actor = await event.locals.pb.collection("activitypub_actors").getFirstListItem(`preferred_username:lower='${username?.toLowerCase()}'&&is_local=true`)
|
||||
|
||||
const filter = `actor='${actor.iri}'&&type='Create'${safeSearchParams.filter ? '&&' + safeSearchParams.filter : ''}`
|
||||
const activities: ListResult<Activity> = await event.locals.pb.collection("activitypub_activities").getList(page, perPage, { sort: safeSearchParams.sort ?? "-created", filter })
|
||||
|
||||
@@ -61,7 +61,7 @@ export async function PUT(event: RequestEvent) {
|
||||
const followerActor: Actor = await event.locals.pb.collection("activitypub_actors").getFirstListItem(`user = '${event.locals.user.id}'`)
|
||||
const followeeActor: Actor = await event.locals.pb.collection("activitypub_actors").getOne(safeData.followee);
|
||||
|
||||
const follow = await event.locals.pb.collection("follows").create({ follower: followerActor.id, followee: followeeActor.id, status: followeeActor.isLocal ? "accepted" : "pending" })
|
||||
const follow = await event.locals.pb.collection("follows").create({ follower: followerActor.id, followee: followeeActor.id, status: followeeActor.is_local ? "accepted" : "pending" })
|
||||
|
||||
return json(follow);
|
||||
} catch (e) {
|
||||
|
||||
@@ -53,7 +53,7 @@ export async function GET(event: RequestEvent) {
|
||||
const safeSearchParams = RecordListOptionsSchema.parse(searchParams);
|
||||
|
||||
let feed: ListResult<FeedItem>;
|
||||
if (actor.isLocal) {
|
||||
if (actor.is_local) {
|
||||
feed = await event.locals.pb.collection(Collection.profile_feed)
|
||||
.getList<FeedItem>(safeSearchParams.page, safeSearchParams.perPage, { ...safeSearchParams, filter: `actor='${actor.id}'` })
|
||||
} else {
|
||||
|
||||
@@ -59,7 +59,7 @@ export async function POST(event: RequestEvent) {
|
||||
const data = await event.request.json()
|
||||
|
||||
let r: SearchResponse<ListSearchResult>;
|
||||
if (actor.isLocal) {
|
||||
if (actor.is_local) {
|
||||
r = await event.locals.ms.index("lists").search(data.q, { ...data.options, filter: `author = ${actor.id}` });
|
||||
} else {
|
||||
const origin = new URL(actor.iri).origin
|
||||
|
||||
@@ -58,7 +58,7 @@ export async function GET(event: RequestEvent) {
|
||||
}
|
||||
|
||||
let summitLogs: SummitLog[];
|
||||
if (actor.isLocal) {
|
||||
if (actor.is_local) {
|
||||
summitLogs = await event.locals.pb.collection(Collection.summit_logs)
|
||||
.getFullList<SummitLog>(safeSearchParams.page, { ...safeSearchParams })
|
||||
} else {
|
||||
|
||||
@@ -58,7 +58,7 @@ export async function POST(event: RequestEvent) {
|
||||
const data = await event.request.json()
|
||||
|
||||
let r: SearchResponse<TrailSearchResult>;
|
||||
if (actor.isLocal) {
|
||||
if (actor.is_local) {
|
||||
r = await event.locals.ms.index("trails").search(data.q, { ...data.options, filter: `author = ${actor.id}` });
|
||||
} else {
|
||||
const origin = new URL(actor.iri).origin
|
||||
|
||||
@@ -1,16 +1,23 @@
|
||||
import type { Actor } from '$lib/models/activitypub/actor';
|
||||
import type { Actor, ActorSearchResult } from '$lib/models/activitypub/actor';
|
||||
import { getActorResponseForHandle } from '$lib/util/activitypub_server_util';
|
||||
import { splitUsername } from '$lib/util/activitypub_util';
|
||||
import { isValidPubHandle, splitUsername } from '$lib/util/activitypub_util';
|
||||
import { handleError } from '$lib/util/api_util';
|
||||
import { error, json, type RequestEvent } from '@sveltejs/kit';
|
||||
import { ClientResponseError, type ListResult } from "pocketbase"
|
||||
import type { SearchResponse } from "meilisearch";
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /api/v1/search/actor:
|
||||
* get:
|
||||
* summary: Search actors
|
||||
* description: Searches for ActivityPub actors by username, combining local and federated results
|
||||
* description: |
|
||||
* Searches for ActivityPub actors by username. If the query is a valid
|
||||
* federated handle (e.g. `user@domain.tld`), it attempts a direct
|
||||
* ActivityPub lookup first and returns that result immediately. If the
|
||||
* handle lookup fails, or if the query is not a handle, it falls back to
|
||||
* a local Meilisearch index query. Returns a Meilisearch-shaped
|
||||
* `SearchResponse` in both cases.
|
||||
* tags:
|
||||
* - Search
|
||||
* parameters:
|
||||
@@ -19,55 +26,126 @@ import { ClientResponseError, type ListResult } from "pocketbase"
|
||||
* required: true
|
||||
* schema:
|
||||
* type: string
|
||||
* description: |
|
||||
* Search query. Can be a plain username substring or a fully-qualified
|
||||
* ActivityPub handle (`user@domain.tld`). Handles trigger a federated
|
||||
* lookup before falling back to local search.
|
||||
* - in: query
|
||||
* name: limit
|
||||
* schema:
|
||||
* type: integer
|
||||
* default: 3
|
||||
* description: |
|
||||
* Maximum number of results to return from the local index. Has no
|
||||
* effect when a federated handle is resolved successfully (always
|
||||
* returns exactly one hit).
|
||||
* - in: query
|
||||
* name: includeSelf
|
||||
* schema:
|
||||
* type: boolean
|
||||
* default: true
|
||||
* description: |
|
||||
* When `false` and the request is authenticated, the authenticated
|
||||
* user's own actor is excluded from local search results.
|
||||
* responses:
|
||||
* 200:
|
||||
* description: Array of matching actors
|
||||
* description: |
|
||||
* Meilisearch-shaped response containing matching actors. The `hits`
|
||||
* array contains actor objects with `id`, `domain`, `is_local`,
|
||||
* `preferred_username`, `username`, and `icon` fields.
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: array
|
||||
* items:
|
||||
* type: object
|
||||
* type: object
|
||||
* properties:
|
||||
* hits:
|
||||
* type: array
|
||||
* items:
|
||||
* type: object
|
||||
* properties:
|
||||
* id:
|
||||
* type: string
|
||||
* domain:
|
||||
* type: string
|
||||
* is_local:
|
||||
* type: boolean
|
||||
* preferred_username:
|
||||
* type: string
|
||||
* username:
|
||||
* type: string
|
||||
* icon:
|
||||
* type: string
|
||||
* query:
|
||||
* type: string
|
||||
* processingTimeMs:
|
||||
* type: integer
|
||||
* estimatedTotalHits:
|
||||
* type: integer
|
||||
* totalHits:
|
||||
* type: integer
|
||||
* totalPages:
|
||||
* type: integer
|
||||
* page:
|
||||
* type: integer
|
||||
* 400:
|
||||
* description: Bad Request
|
||||
* description: Missing required `q` parameter.
|
||||
* 404:
|
||||
* description: Federated fetch failed with a network error.
|
||||
* 500:
|
||||
* description: Internal Server Error
|
||||
* description: Internal server error.
|
||||
*/
|
||||
export async function GET(event: RequestEvent) {
|
||||
if (!event.locals.user) {
|
||||
return error(401, "Unauthorized")
|
||||
}
|
||||
try {
|
||||
|
||||
if (!event.url.searchParams.has("q")) {
|
||||
throw new ClientResponseError({ status: 400, response: "Bad request" });
|
||||
return error(404, "Bad request: missing required parameter 'q'")
|
||||
|
||||
}
|
||||
const q = event.url.searchParams.get("q")
|
||||
const q = event.url.searchParams.get("q")!
|
||||
const limit = event.url.searchParams.get("limit")
|
||||
|
||||
const [user, domain] = splitUsername(q!)
|
||||
if (isValidPubHandle(q)) {
|
||||
try {
|
||||
const { actor } = await getActorResponseForHandle(event, q!);
|
||||
|
||||
let filter = `username~'${user}'`;
|
||||
const actorSearchResult = <ActorSearchResult>{
|
||||
id: actor.id,
|
||||
domain: actor.domain,
|
||||
is_local: actor.is_local,
|
||||
preferred_username: actor.preferred_username,
|
||||
username: actor.username,
|
||||
iri: actor.iri,
|
||||
icon: actor.icon
|
||||
};
|
||||
|
||||
return json(<SearchResponse>{
|
||||
hits: [actorSearchResult],
|
||||
processingTimeMs: 0,
|
||||
query: q,
|
||||
estimatedTotalHits: 1,
|
||||
totalHits: 1,
|
||||
totalPages: 1,
|
||||
page: 1,
|
||||
})
|
||||
} catch (e) {
|
||||
// Actor could not be found via the handle
|
||||
// At least search our local registry
|
||||
}
|
||||
}
|
||||
|
||||
let filterText = "";
|
||||
|
||||
if (event.url.searchParams.get("includeSelf") == "false" && event.locals.pb.authStore.record) {
|
||||
filter += `&& id != "${event.locals.pb.authStore.record.actor}"`
|
||||
filterText = `id != ${event.locals.pb.authStore.record.actor}`
|
||||
}
|
||||
|
||||
const response = await event.locals.pb.collection("activitypub_actors").getList<Actor>(1, 3, { filter: filter })
|
||||
const r = await event.locals.ms.index("actors").search(q, { filter: filterText, limit: limit ?? 3 });
|
||||
|
||||
try {
|
||||
const { actor } = await getActorResponseForHandle(event, q!);
|
||||
|
||||
if (!response.items.find(i => i.iri == actor.iri)) {
|
||||
response.items.push(actor)
|
||||
}
|
||||
|
||||
} catch (e) {
|
||||
|
||||
}
|
||||
|
||||
return json({ items: response.items })
|
||||
return json(r)
|
||||
|
||||
|
||||
} catch (e) {
|
||||
|
||||
@@ -62,7 +62,7 @@
|
||||
<ul class="space-y-4">
|
||||
{#each follows.items as follow}
|
||||
<a
|
||||
href="/profile/@{follow.preferred_username}{follow.isLocal
|
||||
href="/profile/@{follow.preferred_username}{follow.is_local
|
||||
? ''
|
||||
: '@' + follow.domain}"
|
||||
>
|
||||
|
||||
Reference in New Issue
Block a user