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:
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user