Federation (#327)
* initial commit federation * more federation * more federation * more federation * completes follow, accept, undo * add trail create activity * process trail create activity * more trail create activity * adds missing endpoints * adds update and delete activity * adds comment activities * adds activities back * adds summit logs activities * deletes follow counts table * fixes migrations * fixes migrations * fixes migrations * fixes migrations * adds remote profiles * ctd * ctd * we are getting closer... * adds public summit logs * fixes federated trails in lists * adds remote lists * adds list activites * fixes lists * adds notifications * adds iri redirect * fixes comments and summitlogs * fixes follows and profiles * adds asynchronous send * fixes list search * adds encryption key * bug fixes * fixes activity signing * removes custon activity object types * adds html editor * fixes html editor * fixes display issues on mastodon * adds federated sharing * finishes announcements * fixes small summit log issues * adds trail likes * finalizes likes * fixes images for komoot * add disable federation option * adds private profiles * updates docs * adds federated comments * adds trail and comments actvitiypub routes * adds mentions to editor * adds mentions to trails, comments, summit logs * updates theme * updates theme * update docs * update docs * updates docs * fixes various frontend problems * fixes activitypub follows api * updates docs * updates docs --------- Co-authored-by: Christian Beutel <>
This commit is contained in:
@@ -1,30 +0,0 @@
|
||||
import type { Activity } from "$lib/models/activity";
|
||||
import { APIError } from "$lib/util/api_util";
|
||||
import { type ListResult } from "pocketbase";
|
||||
|
||||
let activities: Activity[] = [];
|
||||
|
||||
export async function activities_index(author: string, page: number = 1, perPage: number = 10, f: (url: RequestInfo | URL, config?: RequestInit) => Promise<Response> = fetch) {
|
||||
const r = await f('/api/v1/activity?' + new URLSearchParams({
|
||||
"perPage": perPage.toString(),
|
||||
page: page.toString(),
|
||||
filter: `author="${author}"`,
|
||||
sort: "-created"
|
||||
}), {
|
||||
method: 'GET',
|
||||
})
|
||||
|
||||
if (!r.ok) {
|
||||
const response = await r.json();
|
||||
throw new APIError(r.status, response.message, response.detail)
|
||||
}
|
||||
|
||||
const fetchedActivities: ListResult<Activity> = await r.json();
|
||||
|
||||
const result = page > 1 ? [...activities, ...fetchedActivities.items] : fetchedActivities.items
|
||||
|
||||
activities = result;
|
||||
|
||||
return { ...fetchedActivities, items: result };
|
||||
|
||||
}
|
||||
@@ -4,13 +4,22 @@ import { APIError } from "$lib/util/api_util";
|
||||
import { type ListResult } from "pocketbase";
|
||||
import { get, writable, type Writable } from "svelte/store";
|
||||
import { currentUser } from "./user_store";
|
||||
import { isURL } from "$lib/util/file_util";
|
||||
|
||||
export const comments: Writable<Comment[]> = writable([])
|
||||
|
||||
export async function comments_index(trail: Trail) {
|
||||
let r = await fetch('/api/v1/comment?' + new URLSearchParams({
|
||||
filter: `trail="${trail.id}"`,
|
||||
sort: "-created"
|
||||
export async function comments_index(trailId: string, handle?: string) {
|
||||
let filter: string;
|
||||
if (isURL(trailId)) {
|
||||
filter = `trail="${trailId}"||trail.iri="${trailId}"||trail="${trailId.substring(trailId.length - 15)}"`
|
||||
} else {
|
||||
filter = `trail="${trailId}"`
|
||||
}
|
||||
let r = await fetch(`/api/v1/comment?` + new URLSearchParams({
|
||||
filter,
|
||||
expand: "author",
|
||||
sort: "-created",
|
||||
...(handle ? { handle } : {})
|
||||
}), {
|
||||
method: 'GET',
|
||||
})
|
||||
@@ -33,9 +42,11 @@ export async function comments_create(comment: Comment) {
|
||||
throw Error("Unauthenticated")
|
||||
}
|
||||
|
||||
comment.author = user.authStore.record!.id;
|
||||
comment.author = user.actor
|
||||
|
||||
let r = await fetch('/api/v1/comment', {
|
||||
let r = await fetch('/api/v1/comment?' + new URLSearchParams({
|
||||
expand: "author",
|
||||
}), {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(comment),
|
||||
})
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
import type { Actor } from "$lib/models/activitypub/actor";
|
||||
import type { Follow } from "$lib/models/follow";
|
||||
import { APIError } from "$lib/util/api_util";
|
||||
import { type ListResult } from "pocketbase";
|
||||
|
||||
let follows: Follow[] = [];
|
||||
let follows: Actor[] = [];
|
||||
|
||||
export async function follows_index(data: { follower?: string, followee?: string }, page: number = 1, perPage: number = 10, f: (url: RequestInfo | URL, config?: RequestInit) => Promise<Response> = fetch) {
|
||||
const r = await f('/api/v1/follow?' + new URLSearchParams({
|
||||
filter: data.follower ? `follower='${data.follower}'` : data.followee ? `followee='${data.followee}'` : '',
|
||||
export async function follows_index(data: { username: string, type: "followers" | "following" }, page: number = 1, perPage: number = 10, f: (url: RequestInfo | URL, config?: RequestInit) => Promise<Response> = fetch) {
|
||||
|
||||
const r = await f(`/api/v1/follow?` + new URLSearchParams({
|
||||
handle: data.username,
|
||||
type: data.type,
|
||||
page: page.toString(),
|
||||
perPage: perPage.toString(),
|
||||
requestKey: "page"
|
||||
}), {
|
||||
method: 'GET',
|
||||
})
|
||||
@@ -19,11 +21,11 @@ export async function follows_index(data: { follower?: string, followee?: string
|
||||
throw new APIError(r.status, response.message, response.detail)
|
||||
}
|
||||
|
||||
const fetchedFollows: ListResult<Follow> = await r.json();
|
||||
const fetchedFollows: ListResult<Actor> = await r.json();
|
||||
|
||||
const result = page > 1 ? [...follows, ...fetchedFollows.items] : fetchedFollows.items
|
||||
|
||||
follows = result;
|
||||
follows = result;
|
||||
|
||||
return { ...fetchedFollows, items: result };
|
||||
}
|
||||
@@ -45,25 +47,10 @@ export async function follows_a_b(a: string, b: string, f: (url: RequestInfo | U
|
||||
return response.items.at(0);
|
||||
}
|
||||
|
||||
export async function follows_counts(id: string, f: (url: RequestInfo | URL, config?: RequestInit) => Promise<Response> = fetch) {
|
||||
const r = await f('/api/v1/follow/count/' + id, {
|
||||
method: 'GET',
|
||||
})
|
||||
|
||||
if (!r.ok) {
|
||||
const response = await r.json();
|
||||
throw new APIError(r.status, response.message, response.detail)
|
||||
}
|
||||
|
||||
const response: { followers: number, following: number } = await r.json();
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
export async function follows_create(follow: Follow) {
|
||||
export async function follows_create(followee: string) {
|
||||
let r = await fetch('/api/v1/follow', {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(follow),
|
||||
body: JSON.stringify({ followee }),
|
||||
})
|
||||
|
||||
if (!r.ok) {
|
||||
|
||||
@@ -9,6 +9,7 @@ export const shares: Writable<ListShare[]> = writable([])
|
||||
export async function list_share_index(list: string, f: (url: RequestInfo | URL, config?: RequestInit) => Promise<Response> = fetch) {
|
||||
const r = await f('/api/v1/list-share?' + new URLSearchParams({
|
||||
filter: `list='${list}'`,
|
||||
expand: "actor"
|
||||
}), {
|
||||
method: 'GET',
|
||||
})
|
||||
|
||||
@@ -1,21 +1,22 @@
|
||||
import { ExpandType, ExpandTypeToString, List, type ListFilter } from "$lib/models/list";
|
||||
import { List, type ListFilter } from "$lib/models/list";
|
||||
import type { Trail } from "$lib/models/trail";
|
||||
import { APIError } from "$lib/util/api_util";
|
||||
import type { Hits } from "meilisearch";
|
||||
import { type AuthRecord, type ListResult } from "pocketbase";
|
||||
import { type AuthRecord, type ListResult, type RecordModel } from "pocketbase";
|
||||
import { get, writable, type Writable } from "svelte/store";
|
||||
import type { ListSearchResult } from "./search_store";
|
||||
import { fetchGPX } from "./trail_store";
|
||||
import { currentUser } from "./user_store";
|
||||
import { objectToFormData } from "$lib/util/file_util";
|
||||
|
||||
let lists: List[] = []
|
||||
export const list: Writable<List | null> = writable(null)
|
||||
export const listTrail: Writable<Trail | null> = writable(null);
|
||||
|
||||
export async function lists_index(filter?: ListFilter, page: number = 1, perPage: number = 5,
|
||||
f: (url: RequestInfo | URL, config?: RequestInit) => Promise<Response> = fetch,
|
||||
e: ExpandType = ExpandType.All) {
|
||||
f: (url: RequestInfo | URL, config?: RequestInit) => Promise<Response> = fetch) {
|
||||
const user = get(currentUser)
|
||||
|
||||
|
||||
const filterText = filter ? buildFilterText(user, filter) : ""
|
||||
|
||||
const r = await f('/api/v1/list?' + new URLSearchParams({
|
||||
@@ -23,7 +24,6 @@ export async function lists_index(filter?: ListFilter, page: number = 1, perPage
|
||||
perPage: perPage.toString(),
|
||||
page: page.toString(),
|
||||
filter: filterText,
|
||||
expand: ExpandTypeToString(e),
|
||||
}), {
|
||||
method: 'GET',
|
||||
})
|
||||
@@ -43,8 +43,8 @@ export async function lists_index(filter?: ListFilter, page: number = 1, perPage
|
||||
}
|
||||
|
||||
|
||||
export async function lists_search_filter(filter: ListFilter, page: number = 1, perPage: number = 5, f: (url: RequestInfo | URL, config?: RequestInit) => Promise<Response> = fetch): Promise<ListResult<List>> {
|
||||
const user = get(currentUser)
|
||||
export async function lists_search_filter(filter: ListFilter, page: number = 1, perPage: number = 5, f: (url: RequestInfo | URL, config?: RequestInit) => Promise<Response> = fetch, user?: AuthRecord) {
|
||||
user ??= get(currentUser)
|
||||
|
||||
const filterText = buildSearchFilterText(user, filter)
|
||||
|
||||
@@ -65,43 +65,21 @@ export async function lists_search_filter(filter: ListFilter, page: number = 1,
|
||||
throw new APIError(r.status, response.message, response.detail)
|
||||
}
|
||||
|
||||
const searchResult: { page: number, totalPages: number, hits: Hits<Record<string, any>> } = await r.json();
|
||||
const result: { page: number, totalPages: number, hits: Hits<ListSearchResult> } = await r.json();
|
||||
|
||||
|
||||
const listIds = searchResult.hits.map((h: Record<string, any>) => h.id);
|
||||
const resultLists: List[] = await searchResultToLists(result.hits)
|
||||
|
||||
if (listIds.length == 0) {
|
||||
return { items: [], page: searchResult.page, perPage, totalItems: 0, totalPages: searchResult.totalPages };
|
||||
}
|
||||
return { items: resultLists, ...result };
|
||||
|
||||
r = await f('/api/v1/list?' + new URLSearchParams({
|
||||
expand: "trails,trails.waypoints,trails.category,list_share_via_list",
|
||||
filter: `'${listIds.join(',')}'~id`,
|
||||
sort: filter.sort && filter.sortOrder ? `${filter.sortOrder}${filter.sort}` : ''
|
||||
}), {
|
||||
method: 'GET',
|
||||
})
|
||||
|
||||
if (!r.ok) {
|
||||
const response = await r.json();
|
||||
throw new APIError(r.status, response.message, response.detail)
|
||||
}
|
||||
|
||||
const fetchedLists: ListResult<List> = await r.json();
|
||||
|
||||
const result = page > 1 ? [...lists, ...fetchedLists.items] : fetchedLists.items
|
||||
|
||||
lists = result;
|
||||
|
||||
return { ...fetchedLists, items: result };
|
||||
|
||||
}
|
||||
|
||||
export async function lists_show(id: string, f: (url: RequestInfo | URL, config?: RequestInit) => Promise<Response> = fetch
|
||||
, e: ExpandType = ExpandType.All) {
|
||||
export async function lists_show(id: string, handle?: string, f: (url: RequestInfo | URL, config?: RequestInit) => Promise<Response> = fetch) {
|
||||
|
||||
const r = await f(`/api/v1/list/${id}?` + new URLSearchParams({
|
||||
expand: ExpandTypeToString(e)
|
||||
expand: "author,trails,trails.author,list_share_via_list.actor",
|
||||
...(handle ? { handle } : {})
|
||||
}), {
|
||||
method: 'GET',
|
||||
})
|
||||
@@ -118,9 +96,6 @@ export async function lists_show(id: string, f: (url: RequestInfo | URL, config?
|
||||
trail.expand.gpx_data = gpxData;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
list.set(response);
|
||||
|
||||
return response;
|
||||
@@ -132,11 +107,17 @@ export async function lists_create(list: List, avatar?: File) {
|
||||
if (!user) {
|
||||
throw Error("Unauthenticated")
|
||||
}
|
||||
list.author = user.id;
|
||||
list.author = user.actor;
|
||||
|
||||
let r = await fetch('/api/v1/list', {
|
||||
const formData = objectToFormData(list)
|
||||
|
||||
if (avatar) {
|
||||
formData.append("avatar", avatar);
|
||||
}
|
||||
|
||||
let r = await fetch('/api/v1/list/form', {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(list),
|
||||
body: formData,
|
||||
})
|
||||
|
||||
if (!r.ok) {
|
||||
@@ -146,45 +127,18 @@ export async function lists_create(list: List, avatar?: File) {
|
||||
|
||||
const model: List = await r.json();
|
||||
|
||||
const formData = new FormData();
|
||||
|
||||
if (avatar) {
|
||||
formData.append("avatar", avatar);
|
||||
}
|
||||
|
||||
r = await fetch(`/api/v1/list/${model.id!}/file`, {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
})
|
||||
|
||||
if (!r.ok) {
|
||||
const response = await r.json();
|
||||
throw new APIError(r.status, response.message, response.detail)
|
||||
}
|
||||
|
||||
return await r.json();
|
||||
return model;
|
||||
}
|
||||
|
||||
export async function lists_update(list: List, avatar?: File) {
|
||||
let r = await fetch('/api/v1/list/' + list.id, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(list),
|
||||
})
|
||||
|
||||
if (!r.ok) {
|
||||
const response = await r.json();
|
||||
throw new APIError(r.status, response.message, response.detail)
|
||||
}
|
||||
|
||||
const model: List = await r.json();
|
||||
|
||||
const formData = new FormData();
|
||||
const formData = objectToFormData(list)
|
||||
|
||||
if (avatar) {
|
||||
formData.append("avatar", avatar);
|
||||
}
|
||||
|
||||
r = await fetch(`/api/v1/list/${model.id!}/file`, {
|
||||
let r = await fetch('/api/v1/list/form/' + list.id, {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
})
|
||||
@@ -193,6 +147,9 @@ export async function lists_update(list: List, avatar?: File) {
|
||||
const response = await r.json();
|
||||
throw new APIError(r.status, response.message, response.detail)
|
||||
}
|
||||
|
||||
const model: List = await r.json();
|
||||
return model;
|
||||
}
|
||||
|
||||
export async function lists_add_trail(list: List, trail: Trail) {
|
||||
@@ -275,17 +232,17 @@ function buildSearchFilterText(user: AuthRecord, filter: ListFilter): string {
|
||||
if (filter.public !== undefined) {
|
||||
filterText += `(public = ${filter.public}`
|
||||
|
||||
if (!filter.author?.length || filter.author == user?.id) {
|
||||
filterText += ` OR author = ${user?.id}`
|
||||
if (!filter.author?.length || filter.author == user?.actor) {
|
||||
filterText += ` OR author = ${user?.actor}`
|
||||
}
|
||||
filterText += ")"
|
||||
}
|
||||
|
||||
if (filter.shared !== undefined) {
|
||||
if (filter.shared === true) {
|
||||
filterText += ` OR shares = ${user?.id}`
|
||||
filterText += ` OR shares = ${user?.actor}`
|
||||
} else {
|
||||
filterText += ` AND NOT shares = ${user?.id}`
|
||||
filterText += ` AND NOT shares = ${user?.actor}`
|
||||
|
||||
}
|
||||
}
|
||||
@@ -293,4 +250,44 @@ function buildSearchFilterText(user: AuthRecord, filter: ListFilter): string {
|
||||
}
|
||||
|
||||
return filterText
|
||||
}
|
||||
|
||||
export async function searchResultToLists(hits: Hits<ListSearchResult>): Promise<List[]> {
|
||||
const lists: List[] = []
|
||||
for (const h of hits) {
|
||||
const l: List & RecordModel = {
|
||||
collectionId: "lists",
|
||||
collectionName: "lists",
|
||||
updated: new Date(h.created * 1000).toISOString(),
|
||||
author: h.author,
|
||||
name: h.name,
|
||||
public: h.public,
|
||||
description: h.description,
|
||||
id: h.id,
|
||||
trails: Array(h.trails).fill("000000000000000"),
|
||||
avatar: h.avatar,
|
||||
elevation_gain: h.elevation_gain,
|
||||
elevation_loss: h.elevation_loss,
|
||||
distance: h.distance,
|
||||
duration: h.duration,
|
||||
iri: h.iri,
|
||||
expand: {
|
||||
author: {
|
||||
icon: h.author_avatar,
|
||||
id: h.author,
|
||||
username: h.author_name,
|
||||
} as any,
|
||||
list_share_via_list: h.shares?.map(s => ({
|
||||
permission: "view",
|
||||
list: h.id,
|
||||
actor: s,
|
||||
})),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
lists.push(l)
|
||||
}
|
||||
|
||||
return lists
|
||||
}
|
||||
@@ -8,6 +8,7 @@ export async function notifications_index(data: { recipient: string, seen?: bool
|
||||
const r = await f('/api/v1/notification?' + new URLSearchParams({
|
||||
filter: `created<=@month&&recipient='${data.recipient}'` + (data.seen !== undefined ? `&&seen=${data.seen}` : ''),
|
||||
sort: '+seen,-created',
|
||||
expand: 'author',
|
||||
page: page.toString(),
|
||||
"perPage": perPage.toString()
|
||||
}), {
|
||||
|
||||
136
web/src/lib/stores/profile_store.ts
Normal file
136
web/src/lib/stores/profile_store.ts
Normal file
@@ -0,0 +1,136 @@
|
||||
import type { TimelineItem } from "$lib/models/timeline";
|
||||
import { defaultTrailSearchAttributes, Trail, type TrailFilter, type TrailSearchResult } from "$lib/models/trail";
|
||||
import { APIError } from "$lib/util/api_util";
|
||||
import type { Hits } from "meilisearch";
|
||||
import type { ListResult } from "pocketbase";
|
||||
import { searchResultToTrailList } from "./trail_store";
|
||||
import type { SummitLog, SummitLogFilter } from "$lib/models/summit_log";
|
||||
import { buildFilterText } from "./summit_log_store";
|
||||
import type { ListFilter } from "$lib/models/list";
|
||||
import type { ListSearchResult } from "./search_store";
|
||||
import { searchResultToLists } from "./list_store";
|
||||
|
||||
let timeline: TimelineItem[] = []
|
||||
|
||||
|
||||
export async function profile_show(handle: string, f: (url: RequestInfo | URL, config?: RequestInit) => Promise<Response> = fetch) {
|
||||
let r = await f('/api/v1/profile/' + handle, {
|
||||
method: 'GET',
|
||||
})
|
||||
if (!r.ok) {
|
||||
const response = await r.json();
|
||||
throw new APIError(r.status, response.message, response.detail)
|
||||
}
|
||||
|
||||
const response = await r.json()
|
||||
return response;
|
||||
|
||||
}
|
||||
|
||||
export async function profile_lists_index(handle: string, filter: ListFilter, page: number = 1, perPage: number = 6, f: (url: RequestInfo | URL, config?: RequestInit) => Promise<Response> = fetch) {
|
||||
let r = await f(`/api/v1/profile/${handle}/lists`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
q: filter.q,
|
||||
options: {
|
||||
sort: [`${filter.sort}:${filter.sortOrder == "+" ? "asc" : "desc"}`],
|
||||
hitsPerPage: perPage,
|
||||
page: page
|
||||
}
|
||||
}),
|
||||
});
|
||||
|
||||
if (!r.ok) {
|
||||
const response = await r.json();
|
||||
throw new APIError(r.status, response.message, response.detail)
|
||||
}
|
||||
|
||||
const result: { page: number, totalPages: number, hits: Hits<ListSearchResult> } = await r.json();
|
||||
|
||||
if (result.hits.length == 0) {
|
||||
return { items: [], ...result };
|
||||
}
|
||||
|
||||
const resultLists = await searchResultToLists(result.hits)
|
||||
|
||||
return { items: resultLists, ...result };
|
||||
|
||||
}
|
||||
|
||||
|
||||
export async function profile_timeline_index(handle: string, page: number, perPage: number = 1, f: (url: RequestInfo | URL, config?: RequestInit) => Promise<Response> = fetch) {
|
||||
let r = await f(`/api/v1/profile/${handle}/timeline?` + new URLSearchParams({
|
||||
page: page.toString(),
|
||||
perPage: perPage.toString(),
|
||||
sort: '-created'
|
||||
}), {
|
||||
method: 'GET',
|
||||
})
|
||||
if (!r.ok) {
|
||||
const response = await r.json();
|
||||
throw new APIError(r.status, response.message, response.detail)
|
||||
}
|
||||
|
||||
const fetchedTimeline: ListResult<TimelineItem> = await r.json()
|
||||
|
||||
const result = page > 1 ? [...timeline, ...fetchedTimeline.items] : fetchedTimeline.items
|
||||
|
||||
timeline = result;
|
||||
|
||||
return { ...fetchedTimeline, items: result };
|
||||
|
||||
}
|
||||
|
||||
export async function profile_trails_index(handle: string, filter: TrailFilter, page: number = 1, perPage: number = 1, f: (url: RequestInfo | URL, config?: RequestInit) => Promise<Response> = fetch) {
|
||||
let r = await f(`/api/v1/profile/${handle}/trails`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
q: filter.q,
|
||||
options: {
|
||||
attributesToRetrieve: defaultTrailSearchAttributes,
|
||||
sort: [`${filter.sort}:${filter.sortOrder == "+" ? "asc" : "desc"}`],
|
||||
hitsPerPage: 12,
|
||||
page: page
|
||||
}
|
||||
}),
|
||||
});
|
||||
|
||||
if (!r.ok) {
|
||||
const response = await r.json();
|
||||
throw new APIError(r.status, response.message, response.detail)
|
||||
}
|
||||
|
||||
const result: { page: number, totalPages: number, hits: Hits<TrailSearchResult> } = await r.json();
|
||||
|
||||
if (result.hits.length == 0) {
|
||||
return { items: [], ...result };
|
||||
}
|
||||
|
||||
const resultTrails: Trail[] = await searchResultToTrailList(result.hits)
|
||||
|
||||
return { items: resultTrails, ...result };
|
||||
|
||||
}
|
||||
|
||||
export async function profile_stats_index(handle: string, filter: SummitLogFilter, f: (url: RequestInfo | URL, config?: RequestInit) => Promise<Response> = fetch) {
|
||||
const filterText = buildFilterText(filter);
|
||||
|
||||
const r = await f(`/api/v1/profile/${handle}/stats?` + new URLSearchParams({
|
||||
filter: filterText,
|
||||
perPage: "-1",
|
||||
expand: "trail.category,author",
|
||||
sort: "+date",
|
||||
}), {
|
||||
method: 'GET',
|
||||
})
|
||||
|
||||
if (!r.ok) {
|
||||
const response = await r.json();
|
||||
throw new APIError(r.status, response.message, response.detail)
|
||||
}
|
||||
|
||||
const result: ListResult<SummitLog> = await r.json();
|
||||
|
||||
return result.items;
|
||||
|
||||
}
|
||||
@@ -1,7 +1,9 @@
|
||||
import { env } from "$env/dynamic/public";
|
||||
import { defaultTrailSearchAttributes } from "$lib/models/trail";
|
||||
import type { Actor } 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;
|
||||
@@ -12,37 +14,23 @@ export type LocationSearchResult = {
|
||||
type: string;
|
||||
}
|
||||
|
||||
export type TrailSearchResult = {
|
||||
id: string;
|
||||
_geo: {
|
||||
lat: number,
|
||||
lon: number
|
||||
}
|
||||
author: string;
|
||||
category: string;
|
||||
completed: boolean;
|
||||
created: number;
|
||||
date: number;
|
||||
description: string;
|
||||
difficulty: "easy" | "moderate" | "difficult"
|
||||
distance: number;
|
||||
duration: number
|
||||
elevation_gain: number;
|
||||
elevation_loss: number
|
||||
location: string;
|
||||
name: string;
|
||||
public: boolean;
|
||||
polyline?: string;
|
||||
}
|
||||
|
||||
export type ListSearchResult = {
|
||||
id: string;
|
||||
author: string;
|
||||
author_name: string;
|
||||
author_avatar: string;
|
||||
avatar?: string;
|
||||
created: number;
|
||||
description: string;
|
||||
name: string;
|
||||
elevation_gain: number;
|
||||
elevation_loss: number;
|
||||
distance: number,
|
||||
duration: number,
|
||||
public: boolean;
|
||||
trails: string[]
|
||||
trails: number
|
||||
shares?: string[];
|
||||
iri?: string;
|
||||
}
|
||||
|
||||
type NominatimResponse = {
|
||||
@@ -202,4 +190,21 @@ export async function searchMulti(options: MultiSearchParams): Promise<MultiSear
|
||||
|
||||
|
||||
return response.results
|
||||
}
|
||||
|
||||
export async function searchActors(q: string, includeSelf: boolean = true): Promise<Actor[]> {
|
||||
try {
|
||||
const r = await fetch(`/api/v1/search/actor?q=${q}&includeSelf=${includeSelf}`,)
|
||||
|
||||
if (!r.ok) {
|
||||
return []
|
||||
}
|
||||
const response: ListResult<Actor> = await r.json()
|
||||
|
||||
return response.items
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
|
||||
return []
|
||||
}
|
||||
}
|
||||
@@ -3,20 +3,19 @@ import { APIError } from "$lib/util/api_util";
|
||||
import { type AuthRecord, type ListResult } from "pocketbase";
|
||||
import { get, writable, type Writable } from "svelte/store";
|
||||
import { currentUser } from "./user_store";
|
||||
import { isURL, objectToFormData } from "$lib/util/file_util";
|
||||
|
||||
export const summitLog: Writable<SummitLog> = writable(new SummitLog(new Date().toISOString().substring(0, 10)));
|
||||
export const summitLogs: Writable<SummitLog[]> = writable([]);
|
||||
|
||||
export async function summit_logs_index(author: string, filter?: SummitLogFilter, f: (url: RequestInfo | URL, config?: RequestInit) => Promise<Response> = fetch) {
|
||||
|
||||
let filterText = `author='${author}'`
|
||||
filterText += filter ? "&&" + buildFilterText(filter) : "";
|
||||
export async function summit_logs_index(filter?: SummitLogFilter, handle?: string, f: (url: RequestInfo | URL, config?: RequestInit) => Promise<Response> = fetch) {
|
||||
|
||||
const r = await f('/api/v1/summit-log?' + new URLSearchParams({
|
||||
filter: filterText,
|
||||
...(filter ? { filter: buildFilterText(filter) } : {}),
|
||||
perPage: "-1",
|
||||
expand: "trails_via_summit_logs.category",
|
||||
expand: "trail.category,author",
|
||||
sort: "+date",
|
||||
...(handle ? { handle } : {})
|
||||
}), {
|
||||
method: 'GET',
|
||||
})
|
||||
@@ -28,18 +27,6 @@ export async function summit_logs_index(author: string, filter?: SummitLogFilter
|
||||
|
||||
const fetchedSummitLogs: ListResult<SummitLog> = await r.json();
|
||||
|
||||
// for (const log of fetchedSummitLogs.items) {
|
||||
// if (!log.gpx) {
|
||||
// continue
|
||||
// }
|
||||
// const gpxData: string = await fetchGPX(log as any, f);
|
||||
|
||||
// if (!log.expand) {
|
||||
// log.expand = {};
|
||||
// }
|
||||
// log.expand.gpx_data = gpxData;
|
||||
// }
|
||||
|
||||
summitLogs.set(fetchedSummitLogs.items);
|
||||
|
||||
return fetchedSummitLogs;
|
||||
@@ -51,11 +38,27 @@ export async function summit_logs_create(summitLog: SummitLog, f: (url: RequestI
|
||||
throw Error("Unauthenticated")
|
||||
}
|
||||
|
||||
summitLog.author = user.id
|
||||
summitLog.author = user.actor
|
||||
|
||||
let r = await f('/api/v1/summit-log', {
|
||||
const formData = objectToFormData(summitLog, ["expand"])
|
||||
|
||||
if (summitLog._gpx && summitLog._gpx instanceof File) {
|
||||
formData.append("gpx", summitLog._gpx)
|
||||
}
|
||||
|
||||
|
||||
if (summitLog._photos && summitLog._photos.length) {
|
||||
|
||||
for (const photo of summitLog._photos) {
|
||||
formData.append("photos", photo)
|
||||
}
|
||||
}
|
||||
|
||||
let r = await f('/api/v1/summit-log/form?' + new URLSearchParams({
|
||||
expand: "author"
|
||||
}), {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(summitLog),
|
||||
body: formData,
|
||||
})
|
||||
|
||||
if (!r.ok) {
|
||||
@@ -65,43 +68,6 @@ export async function summit_logs_create(summitLog: SummitLog, f: (url: RequestI
|
||||
|
||||
let model: SummitLog = await r.json();
|
||||
|
||||
if (summitLog._gpx && summitLog._gpx instanceof File) {
|
||||
|
||||
const formData = new FormData()
|
||||
|
||||
formData.append("gpx", summitLog._gpx)
|
||||
|
||||
r = await f(`/api/v1/summit-log/${model.id!}/file`, {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
})
|
||||
|
||||
if (!r.ok) {
|
||||
const response = await r.json();
|
||||
throw new APIError(r.status, response.message, response.detail)
|
||||
}
|
||||
}
|
||||
|
||||
if (summitLog._photos && summitLog._photos.length) {
|
||||
|
||||
const formData = new FormData()
|
||||
|
||||
for (const photo of summitLog._photos) {
|
||||
formData.append("photos", photo)
|
||||
}
|
||||
|
||||
r = await fetch(`/api/v1/summit-log/${model.id!}/file`, {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
})
|
||||
|
||||
if (!r.ok) {
|
||||
const response = await r.json();
|
||||
throw new APIError(r.status, response.message, response.detail)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return model;
|
||||
}
|
||||
|
||||
@@ -111,19 +77,9 @@ export async function summit_logs_update(oldSummitLog: SummitLog, newSummitLog:
|
||||
throw Error("Unauthenticated")
|
||||
}
|
||||
|
||||
newSummitLog.author = user.id
|
||||
newSummitLog.author = user.actor
|
||||
|
||||
let r = await fetch('/api/v1/summit-log/' + newSummitLog.id, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(newSummitLog),
|
||||
})
|
||||
|
||||
if (!r.ok) {
|
||||
const response = await r.json();
|
||||
throw new APIError(r.status, response.message, response.detail)
|
||||
}
|
||||
|
||||
const formData = new FormData()
|
||||
const formData = objectToFormData(newSummitLog, ["expand"])
|
||||
|
||||
for (const photo of newSummitLog._photos ?? []) {
|
||||
formData.append("photos", photo)
|
||||
@@ -139,7 +95,9 @@ export async function summit_logs_update(oldSummitLog: SummitLog, newSummitLog:
|
||||
formData.append("gpx", newSummitLog._gpx);
|
||||
}
|
||||
|
||||
r = await fetch(`/api/v1/summit-log/${newSummitLog.id!}/file`, {
|
||||
let r = await fetch('/api/v1/summit-log/form/' + newSummitLog.id + '?' + new URLSearchParams({
|
||||
expand: "author"
|
||||
}), {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
})
|
||||
@@ -165,11 +123,11 @@ export async function summit_logs_delete(summitLog: SummitLog) {
|
||||
|
||||
}
|
||||
|
||||
function buildFilterText(filter: SummitLogFilter,): string {
|
||||
export function buildFilterText(filter: SummitLogFilter,): string {
|
||||
let filterText: string = "";
|
||||
|
||||
if (filter.category.length > 0) {
|
||||
filterText += `trails_via_summit_logs.category!=null&&'${filter.category.join(",")}'~trails_via_summit_logs.category`;
|
||||
filterText += `trail.category!=null&&'${filter.category.join(",")}'~trail.category`;
|
||||
}
|
||||
|
||||
if (filter.startDate) {
|
||||
@@ -180,6 +138,17 @@ function buildFilterText(filter: SummitLogFilter,): string {
|
||||
filterText += `${filter.category.length || filter.startDate ? '&&' : ''}date<='${filter.endDate}'`
|
||||
}
|
||||
|
||||
if (filter.trail) {
|
||||
if (filter.category.length || filter.startDate || filter.endDate) {
|
||||
filterText += "&&"
|
||||
}
|
||||
if (isURL(filter.trail)) {
|
||||
filterText += `trail='${filter.trail}'||trail.iri='${filter.trail}'||trail='${filter.trail.substring(filter.trail.length - 15)}'`;
|
||||
} else {
|
||||
filterText += `trail='${filter.trail}'`
|
||||
}
|
||||
}
|
||||
|
||||
return filterText;
|
||||
|
||||
}
|
||||
56
web/src/lib/stores/trail_like_store.ts
Normal file
56
web/src/lib/stores/trail_like_store.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import type { TrailLike } from "$lib/models/trail_like";
|
||||
import { APIError } from "$lib/util/api_util";
|
||||
import { type ListResult } from "pocketbase";
|
||||
|
||||
export async function trail_like_index(data: { trail?: string, actor?: string }, f: (url: RequestInfo | URL, config?: RequestInit) => Promise<Response> = fetch) {
|
||||
let filter = ""
|
||||
if (data.trail) {
|
||||
filter += `trail='${data.trail}'`
|
||||
} else if (data.actor) {
|
||||
filter += `actor='${data.actor}'`
|
||||
}
|
||||
|
||||
const r = await f('/api/v1/trail-like?' + new URLSearchParams({
|
||||
filter: filter,
|
||||
expand: "actor"
|
||||
}), {
|
||||
method: 'GET',
|
||||
})
|
||||
|
||||
if (!r.ok) {
|
||||
const response = await r.json();
|
||||
throw new APIError(r.status, response.message, response.detail)
|
||||
}
|
||||
|
||||
const response: ListResult<TrailLike> = await r.json();
|
||||
|
||||
return response.items;
|
||||
}
|
||||
|
||||
export async function trail_like_create(like: TrailLike) {
|
||||
let r = await fetch('/api/v1/trail-like', {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(like),
|
||||
})
|
||||
|
||||
if (!r.ok) {
|
||||
const response = await r.json();
|
||||
throw new APIError(r.status, response.message, response.detail)
|
||||
}
|
||||
|
||||
const response: TrailLike = await r.json()
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
export async function trail_like_delete(like: TrailLike) {
|
||||
const r = await fetch('/api/v1/trail-like/delete', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(like)
|
||||
})
|
||||
|
||||
if (!r.ok) {
|
||||
const response = await r.json();
|
||||
throw new APIError(r.status, response.message, response.detail)
|
||||
}
|
||||
}
|
||||
@@ -6,9 +6,19 @@ import { writable, type Writable } from "svelte/store";
|
||||
export const shares: Writable<TrailShare[]> = writable([])
|
||||
|
||||
|
||||
export async function trail_share_index(data: { trail?: string, user?: string }, f: (url: RequestInfo | URL, config?: RequestInit) => Promise<Response> = fetch) {
|
||||
export async function trail_share_index(data: { trail?: string, actor?: string, actorIRI?: string }, f: (url: RequestInfo | URL, config?: RequestInit) => Promise<Response> = fetch) {
|
||||
let filter = ""
|
||||
if (data.trail) {
|
||||
filter += `trail='${data.trail}'`
|
||||
} else if (data.actor) {
|
||||
filter += `actor='${data.actor}'`
|
||||
} else if (data.actorIRI) {
|
||||
filter += `actor.iri='${data.actorIRI}'`
|
||||
}
|
||||
|
||||
const r = await f('/api/v1/trail-share?' + new URLSearchParams({
|
||||
filter: data.trail ? `trail='${data.trail}'` : data.user ? `user='${data.user}'` : '',
|
||||
filter: filter,
|
||||
expand: "actor"
|
||||
}), {
|
||||
method: 'GET',
|
||||
})
|
||||
|
||||
@@ -4,7 +4,7 @@ import { defaultTrailSearchAttributes, Trail, type TrailFilter, type TrailFilter
|
||||
import type { Waypoint } from "$lib/models/waypoint";
|
||||
import { APIError } from "$lib/util/api_util";
|
||||
import { deepEqual } from "$lib/util/deep_util";
|
||||
import { getFileURL } from "$lib/util/file_util";
|
||||
import { getFileURL, objectToFormData } from "$lib/util/file_util";
|
||||
import * as M from "maplibre-gl";
|
||||
import type { Hits } from "meilisearch";
|
||||
import { type AuthRecord, type ListResult, type RecordModel } from "pocketbase";
|
||||
@@ -22,7 +22,7 @@ export const editTrail: Writable<Trail> = writable(new Trail(""));
|
||||
export async function trails_index(perPage: number = 21, random: boolean = false, f: (url: RequestInfo | URL, config?: RequestInit) => Promise<Response> = fetch) {
|
||||
const r = await f('/api/v1/trail?' + new URLSearchParams({
|
||||
"perPage": perPage.toString(),
|
||||
expand: "category,waypoints,summit_logs,tags",
|
||||
expand: "category,waypoints,summit_logs_via_trail,tags",
|
||||
sort: random ? "@random" : "",
|
||||
}), {
|
||||
method: 'GET',
|
||||
@@ -50,9 +50,9 @@ export async function trails_recommend(size: number, f: (url: RequestInfo | URL,
|
||||
const response = await r.json();
|
||||
throw new APIError(r.status, response.message, response.detail)
|
||||
}
|
||||
const response: Trail[] = await r.json()
|
||||
const response: Hits<TrailSearchResult> = await r.json()
|
||||
|
||||
return response;
|
||||
return searchResultToTrailList(response);
|
||||
|
||||
}
|
||||
|
||||
@@ -132,9 +132,12 @@ export async function trails_search_bounding_box(northEast: M.LngLat, southWest:
|
||||
|
||||
}
|
||||
|
||||
export async function trails_show(id: string, loadGPX?: boolean, f: (url: RequestInfo | URL, config?: RequestInit) => Promise<Response> = fetch) {
|
||||
export async function trails_show(id: string, handle?: string, loadGPX?: boolean, f: (url: RequestInfo | URL, config?: RequestInit) => Promise<Response> = fetch) {
|
||||
|
||||
const r = await f(`/api/v1/trail/${id}?` + new URLSearchParams({
|
||||
expand: "category,waypoints,summit_logs,trail_share_via_trail,tags",
|
||||
expand: "category,waypoints,summit_logs_via_trail,summit_logs_via_trail.author,trail_share_via_trail.actor,trail_like_via_trail,tags,author",
|
||||
...(handle ? { handle } : {})
|
||||
|
||||
}), {
|
||||
method: 'GET',
|
||||
})
|
||||
@@ -157,7 +160,7 @@ export async function trails_show(id: string, loadGPX?: boolean, f: (url: Reques
|
||||
response.expand.gpx_data = gpxData;
|
||||
|
||||
|
||||
for (const log of response.expand.summit_logs ?? []) {
|
||||
for (const log of response.expand.summit_logs_via_trail ?? []) {
|
||||
const gpxData: string = await fetchGPX(log, f);
|
||||
|
||||
if (!log.expand) {
|
||||
@@ -188,10 +191,6 @@ export async function trails_create(trail: Trail, photos: File[], gpx: File | Bl
|
||||
}, f, user);
|
||||
trail.waypoints.push(model.id!);
|
||||
}
|
||||
for (const summitLog of trail.expand?.summit_logs ?? []) {
|
||||
const model = await summit_logs_create(summitLog, f);
|
||||
trail.summit_logs.push(model.id!);
|
||||
}
|
||||
for (const tag of trail.expand?.tags ?? []) {
|
||||
if (!tag.id) {
|
||||
const model = await tags_create(tag)
|
||||
@@ -201,34 +200,22 @@ export async function trails_create(trail: Trail, photos: File[], gpx: File | Bl
|
||||
}
|
||||
}
|
||||
|
||||
trail.author = user.id
|
||||
trail.author = user.actor
|
||||
|
||||
let r = await f(`/api/v1/trail?` + new URLSearchParams({
|
||||
expand: "category,waypoints,summit_logs,trail_share_via_trail,tags",
|
||||
}), {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({ ...trail }),
|
||||
})
|
||||
const formData = objectToFormData(trail)
|
||||
|
||||
if (!r.ok) {
|
||||
const response = await r.json();
|
||||
throw new APIError(r.status, response.message, response.detail)
|
||||
}
|
||||
|
||||
|
||||
let model: Trail = await r.json();
|
||||
|
||||
const formData = new FormData()
|
||||
if (gpx) {
|
||||
formData.append("gpx", gpx);
|
||||
formData.set("gpx", gpx);
|
||||
}
|
||||
|
||||
for (const photo of photos) {
|
||||
formData.append("photos", photo)
|
||||
formData.set("photos", photo)
|
||||
}
|
||||
|
||||
r = await f(`/api/v1/trail/${model.id!}/file`, {
|
||||
method: 'POST',
|
||||
let r = await f(`/api/v1/trail/form?` + new URLSearchParams({
|
||||
expand: "category,waypoints,summit_logs_via_trail,trail_share_via_trail,tags",
|
||||
}), {
|
||||
method: 'PUT',
|
||||
body: formData,
|
||||
})
|
||||
|
||||
@@ -237,15 +224,19 @@ export async function trails_create(trail: Trail, photos: File[], gpx: File | Bl
|
||||
throw new APIError(r.status, response.message, response.detail)
|
||||
}
|
||||
|
||||
const modelWithFiles: Trail = await r.json();
|
||||
let model: Trail = await r.json();
|
||||
|
||||
model.photos = modelWithFiles.photos;
|
||||
for (const summitLog of trail.expand?.summit_logs_via_trail ?? []) {
|
||||
summitLog.trail = model.id!;
|
||||
await summit_logs_create(summitLog, f);
|
||||
}
|
||||
|
||||
return model;
|
||||
|
||||
}
|
||||
|
||||
export async function trails_update(oldTrail: Trail, newTrail: Trail, photos?: File[], gpx?: File | Blob | null) {
|
||||
newTrail.author = oldTrail.author
|
||||
|
||||
const waypointUpdates = compareObjectArrays<Waypoint>(oldTrail.expand?.waypoints ?? [], newTrail.expand?.waypoints ?? []);
|
||||
|
||||
@@ -269,15 +260,15 @@ export async function trails_update(oldTrail: Trail, newTrail: Trail, photos?: F
|
||||
const success = await waypoints_delete(deletedWaypoint);
|
||||
}
|
||||
|
||||
const summitLogUpdates = compareObjectArrays<SummitLog>(oldTrail.expand?.summit_logs ?? [], newTrail.expand?.summit_logs ?? []);
|
||||
const summitLogUpdates = compareObjectArrays<SummitLog>(oldTrail.expand?.summit_logs_via_trail ?? [], newTrail.expand?.summit_logs_via_trail ?? []);
|
||||
|
||||
for (const summitLog of summitLogUpdates.added) {
|
||||
summitLog.trail = newTrail.id!
|
||||
const model = await summit_logs_create(summitLog);
|
||||
newTrail.summit_logs.push(model.id!);
|
||||
}
|
||||
|
||||
for (const updatedSummitLog of summitLogUpdates.updated) {
|
||||
const oldSummitLog = oldTrail.expand?.summit_logs?.find(w => w.id == updatedSummitLog.id);
|
||||
const oldSummitLog = oldTrail.expand?.summit_logs_via_trail?.find(w => w.id == updatedSummitLog.id);
|
||||
|
||||
const model = await summit_logs_update(oldSummitLog!, updatedSummitLog);
|
||||
}
|
||||
@@ -301,22 +292,8 @@ export async function trails_update(oldTrail: Trail, newTrail: Trail, photos?: F
|
||||
newTrail.tags = newTrail.tags.filter(t => t != tag.id);
|
||||
}
|
||||
|
||||
let r = await fetch(`/api/v1/trail/${newTrail.id}?` + new URLSearchParams({
|
||||
expand: "category,waypoints,summit_logs,trail_share_via_trail,tags",
|
||||
}), {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ ...newTrail, expand: undefined }),
|
||||
})
|
||||
const formData = objectToFormData(newTrail, ["expand"])
|
||||
|
||||
if (!r.ok) {
|
||||
const response = await r.json();
|
||||
throw new APIError(r.status, response.message, response.detail)
|
||||
}
|
||||
|
||||
|
||||
let model: Trail = await r.json();
|
||||
|
||||
const formData = new FormData()
|
||||
if (gpx) {
|
||||
formData.append("gpx", gpx);
|
||||
}
|
||||
@@ -327,14 +304,16 @@ export async function trails_update(oldTrail: Trail, newTrail: Trail, photos?: F
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const deletedPhotos = oldTrail.photos.filter(oldPhoto => !newTrail.photos.find(newPhoto => newPhoto === oldPhoto));
|
||||
|
||||
for (const deletedPhoto of deletedPhotos) {
|
||||
formData.append("photos-", deletedPhoto.replace(/^.*[\\/]/, ''));
|
||||
}
|
||||
|
||||
r = await fetch(`/api/v1/trail/${newTrail.id!}/file`, {
|
||||
|
||||
let r = await fetch(`/api/v1/trail/form/${newTrail.id}?` + new URLSearchParams({
|
||||
expand: "category,waypoints,summit_logs_via_trail,trail_share_via_trail,tags",
|
||||
}), {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
})
|
||||
@@ -344,10 +323,14 @@ export async function trails_update(oldTrail: Trail, newTrail: Trail, photos?: F
|
||||
throw new APIError(r.status, response.message, response.detail)
|
||||
}
|
||||
|
||||
const response: Trail = await r.json();
|
||||
|
||||
model.photos = response.photos
|
||||
model.gpx = response.gpx
|
||||
let model: Trail = await r.json();
|
||||
|
||||
for (const log of model.expand?.summit_logs_via_trail ?? []) {
|
||||
if (!log.expand) {
|
||||
log.expand = {};
|
||||
}
|
||||
}
|
||||
|
||||
trail.set(model);
|
||||
|
||||
@@ -361,8 +344,8 @@ export async function trails_delete(trail: Trail) {
|
||||
await waypoints_delete(waypoint);
|
||||
}
|
||||
}
|
||||
if (trail.expand?.summit_logs) {
|
||||
for (const summit_log of trail.expand.summit_logs) {
|
||||
if (trail.expand?.summit_logs_via_trail) {
|
||||
for (const summit_log of trail.expand.summit_logs_via_trail) {
|
||||
await summit_logs_delete(summit_log);
|
||||
}
|
||||
}
|
||||
@@ -455,14 +438,14 @@ export async function fetchGPX(trail: { gpx?: string } & Record<string, any>, f:
|
||||
return gpxData
|
||||
}
|
||||
|
||||
async function searchResultToTrailList(hits: Hits<TrailSearchResult>): Promise<Trail[]> {
|
||||
export async function searchResultToTrailList(hits: Hits<TrailSearchResult>): Promise<Trail[]> {
|
||||
const trails: Trail[] = []
|
||||
for (const h of hits) {
|
||||
const t: Trail & RecordModel = {
|
||||
collectionId: "trails",
|
||||
collectionName: "trails",
|
||||
updated: new Date(h.created * 1000).toISOString(),
|
||||
author: h.author,
|
||||
author: h.author_name,
|
||||
name: h.name,
|
||||
photos: h.thumbnail ? [h.thumbnail] : [],
|
||||
public: h.public,
|
||||
@@ -484,26 +467,35 @@ async function searchResultToTrailList(hits: Hits<TrailSearchResult>): Promise<T
|
||||
location: h.location,
|
||||
gpx: h.gpx,
|
||||
polyline: h.polyline,
|
||||
domain: h.domain,
|
||||
iri: h.iri,
|
||||
thumbnail: 0,
|
||||
like_count: h.like_count,
|
||||
expand: {
|
||||
author: {
|
||||
collectionId: "users",
|
||||
private: false,
|
||||
collectionId: "activitypub_actors",
|
||||
isLocal: (h.domain?.length ?? 0) == 0,
|
||||
id: h.author,
|
||||
avatar: h.author_avatar,
|
||||
username: h.author_name
|
||||
icon: h.author_avatar,
|
||||
username: h.author_name,
|
||||
domain: h.domain,
|
||||
} as any,
|
||||
trail_share_via_trail: h.shares?.map(s => ({
|
||||
permission: "view",
|
||||
trail: h.id,
|
||||
user: s,
|
||||
actor: s,
|
||||
})),
|
||||
trail_like_via_trail: h.likes?.map(l => ({
|
||||
trail: h.id,
|
||||
actor: l,
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
trails.push(t)
|
||||
}
|
||||
|
||||
return trails
|
||||
}
|
||||
|
||||
@@ -569,17 +561,17 @@ function buildFilterText(user: AuthRecord, filter: TrailFilter, includeGeo: bool
|
||||
if (filter.public !== undefined) {
|
||||
filterText += `(public = ${filter.public}`
|
||||
|
||||
if (!filter.author?.length || filter.author == user?.id) {
|
||||
filterText += ` OR author = ${user?.id}`
|
||||
if (!filter.author?.length || filter.author == user?.actor) {
|
||||
filterText += ` OR author = ${user?.actor}`
|
||||
}
|
||||
filterText += ")"
|
||||
}
|
||||
|
||||
if (filter.shared !== undefined) {
|
||||
if (filter.shared === true) {
|
||||
filterText += ` OR shares = ${user?.id}`
|
||||
filterText += ` OR shares = ${user?.actor}`
|
||||
} else {
|
||||
filterText += ` AND NOT shares = ${user?.id}`
|
||||
filterText += ` AND NOT shares = ${user?.actor}`
|
||||
|
||||
}
|
||||
}
|
||||
@@ -587,6 +579,10 @@ function buildFilterText(user: AuthRecord, filter: TrailFilter, includeGeo: bool
|
||||
}
|
||||
*/
|
||||
|
||||
if (filter.liked === true) {
|
||||
filterText += ` AND likes = ${user?.actor}`
|
||||
}
|
||||
|
||||
if (filter.startDate) {
|
||||
filterText += ` AND date >= ${new Date(filter.startDate).getTime() / 1000}`
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ export type Upload = {
|
||||
file: File;
|
||||
status: "enqueued" | "uploading" | "cancelled" | "success" | "error" | "duplicate";
|
||||
error?: string;
|
||||
duplicate?: { id: string, name: string };
|
||||
duplicate?: { id: string, domain: string, name: string };
|
||||
progress: number;
|
||||
function: (f: File, ignoreDuplicates?: boolean, onProgress?: (p: number) => void) => Promise<unknown>
|
||||
};
|
||||
@@ -42,7 +42,7 @@ export async function processUploadQueue(batchSize: number = 3, ignoreDuplicates
|
||||
const u = batch[i];
|
||||
if (r instanceof APIError && r.message == "Duplicate trail") {
|
||||
u.status = "duplicate"
|
||||
u.duplicate = { id: r.detail.id, name: r.detail.name };
|
||||
u.duplicate = { id: r.detail.id, domain: r.detail.domain, name: r.detail.name };
|
||||
} else if (r instanceof APIError) {
|
||||
u.status = "error"
|
||||
u.error = r.message
|
||||
|
||||
@@ -45,14 +45,13 @@ export async function users_show(id: string, f: (url: RequestInfo | URL, config?
|
||||
let r = await f(`/api/v1/user/anonymous/${id}`, {
|
||||
method: 'GET',
|
||||
})
|
||||
const response: UserAnonymous = await r.json()
|
||||
if (!r.ok) {
|
||||
const response = await r.json();
|
||||
throw new APIError(r.status, response.message, response.detail)
|
||||
}
|
||||
const response: UserAnonymous = await r.json()
|
||||
|
||||
return response;
|
||||
|
||||
}
|
||||
|
||||
export async function users_auth_methods(f: (url: RequestInfo | URL, config?: RequestInit) => Promise<Response> = fetch): Promise<AuthMethodsList> {
|
||||
|
||||
Reference in New Issue
Block a user