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:
Flomp
2025-06-21 10:09:22 +02:00
committed by GitHub
parent 742a6d9d30
commit 9276690d60
356 changed files with 57150 additions and 35470 deletions

View File

@@ -1,13 +0,0 @@
import type { Activity } from "$lib/models/activity";
import { Collection, handleError, list } from "$lib/util/api_util";
import { json, type RequestEvent } from "@sveltejs/kit";
export async function GET(event: RequestEvent) {
try {
const r = await list<Activity>(event, Collection.activities);
return json(r)
} catch (e) {
throw handleError(e)
}
}

View File

@@ -0,0 +1,32 @@
import type { Activity } from '$lib/models/activitypub/activity';
import { Collection, handleError, show } from "$lib/util/api_util";
import { json, type RequestEvent } from "@sveltejs/kit";
import type { APActivity, APRoot } from 'activitypub-types';
export async function GET(event: RequestEvent) {
try {
const a = await show<Activity>(event, Collection.activitypub_activities)
const activity: APRoot<APActivity> = {
id: a.iri,
type: a.type,
actor: a.actor,
to: a.to,
cc: a.cc,
published: a.published,
object: a.object
}
const headers = new Headers()
headers.append("Content-Type", "application/activity+json")
return json({
"@context": [
"https://www.w3.org/ns/activitystreams",
], ...activity
}, { headers })
} catch (e: any) {
return handleError(e)
}
}

View File

@@ -0,0 +1,28 @@
import { handleError } from '$lib/util/api_util';
import { json, type RequestEvent } from '@sveltejs/kit';
export async function GET(event: RequestEvent) {
const id = event.params.id;
try {
const comment = await event.locals.pb.send("/activitypub/comment/" + id, {
method: "GET",
fetch: event.fetch,
})
const headers = new Headers()
headers.append("Content-Type", "application/activity+json")
return json({
"@context": [
"https://www.w3.org/ns/activitystreams",
],
...comment
}, { status: 200, headers });
} catch (e) {
return handleError(e)
}
}

View File

@@ -0,0 +1,28 @@
import { handleError } from '$lib/util/api_util';
import { json, type RequestEvent } from '@sveltejs/kit';
export async function GET(event: RequestEvent) {
const id = event.params.id;
try {
const trail = await event.locals.pb.send("/activitypub/trail/" + id, {
method: "GET",
fetch: event.fetch,
})
const headers = new Headers()
headers.append("Content-Type", "application/activity+json")
return json({
"@context": [
"https://www.w3.org/ns/activitystreams",
],
...trail
}, { status: 200, headers });
} catch (e) {
return handleError(e)
}
}

View File

@@ -0,0 +1,65 @@
import { env } from '$env/dynamic/private';
import type { Actor } from '$lib/models/activitypub/actor';
import type { UserAnonymous } from "$lib/models/user";
import { splitUsername } from '$lib/util/activitypub_util';
import { handleError } from '$lib/util/api_util';
import { error, json, type RequestEvent } from '@sveltejs/kit';
import { type APActor, type APRoot } from 'activitypub-types';
export async function GET(event: RequestEvent) {
try {
let fullUsername = event.params.handle;
if (!fullUsername) {
return json({ message: "Bad request" }, { status: 400 });
}
fullUsername = fullUsername.replace(/^@/, "");
const [username, domain] = splitUsername(fullUsername, env.ORIGIN)
const actor: Actor = await event.locals.pb.collection("activitypub_actors").getFirstListItem(`username:lower='${username?.toLowerCase()}'&&isLocal=true`)
const user: UserAnonymous = await event.locals.pb.collection("users_anonymous").getOne(actor.user!)
const id = `${env.ORIGIN}/api/v1/activitypub/user/${username}`
const r: APRoot<APActor & { publicKey: { id: string, owner: string, publicKeyPem: string } }> = {
"@context": [
"https://www.w3.org/ns/activitystreams",
],
id: id,
type: "Person",
inbox: id + '/inbox',
outbox: id + '/outbox',
summary: user.bio,
name: actor.username,
preferredUsername: user.username,
followers: id + '/followers',
following: id + '/following',
url: `${env.ORIGIN}/profile/@${username}`,
published: new Date(user.created ?? "").toISOString(),
icon: {
type: "Image",
url: user.avatar ? `${env.ORIGIN}/api/v1/files/users/${user.id}/${user.avatar}` : undefined
},
publicKey: {
id: id + '#main-key',
owner: id,
publicKeyPem: actor.public_key
}
}
const headers = new Headers()
headers.append("Content-Type", "application/activity+json")
return json(r, { headers });
} catch (e) {
return handleError(e)
}
}

View File

@@ -0,0 +1,59 @@
import { env } from '$env/dynamic/private';
import type { Actor } from '$lib/models/activitypub/actor';
import type { Follow } from '$lib/models/follow';
import { splitUsername } from '$lib/util/activitypub_util';
import { handleError } from '$lib/util/api_util';
import { error, json, type RequestEvent } from '@sveltejs/kit';
import type { APOrderedCollectionPage, APRoot } from 'activitypub-types';
import type { ListResult } from 'pocketbase';
export async function GET(event: RequestEvent) {
try {
const page = event.url.searchParams.get("page") ?? "1"
const intPage = parseInt(page);
let fullUsername = event.params.handle;
if (!fullUsername) {
return json({ message: "Bad request" }, { status: 400 });
}
fullUsername = fullUsername.replace(/^@/, "");
const [username, domain] = splitUsername(fullUsername, env.ORIGIN)
const actor: Actor = await event.locals.pb.collection("activitypub_actors").getFirstListItem(`username:lower='${username?.toLowerCase()}'&&isLocal=true`)
const followers: ListResult<Follow> = await event.locals.pb.collection("follows").getList(intPage, 10, { sort: "-created", filter: `followee='${actor.id}'&&status='accepted'`, expand: "follower" })
const id = actor.iri;
const hasNextPage = intPage * 10 < followers.totalItems;
const followersPage: APRoot<APOrderedCollectionPage> = {
"@context": [
"https://www.w3.org/ns/activitystreams",
],
type: "OrderedCollectionPage",
first: id + "/followers?page=1",
...(intPage > 1 ? { prev: `${id}/followers?page=${intPage - 1}` } : {}),
...(hasNextPage ? { next: `${id}/outbox?page=${intPage + 1}` } : {}),
partOf: id + "/followers",
totalItems: followers.totalItems,
orderedItems: followers.items.map<string>(f => f.expand!.follower.iri)
}
const headers = new Headers()
headers.append("Content-Type", "application/activity+json")
return json(followersPage, { headers });
} catch (e) {
return handleError(e)
}
}

View File

@@ -0,0 +1,60 @@
import { env } from '$env/dynamic/private';
import { env as publicEnv } from '$env/dynamic/public';
import type { Actor } from '$lib/models/activitypub/actor';
import type { Follow } from '$lib/models/follow';
import { splitUsername } from '$lib/util/activitypub_util';
import { handleError } from '$lib/util/api_util';
import { error, json, type RequestEvent } from '@sveltejs/kit';
import type { APOrderedCollectionPage, APRoot } from 'activitypub-types';
import type { ListResult } from 'pocketbase';
export async function GET(event: RequestEvent) {
try {
const page = event.url.searchParams.get("page") ?? "1"
const intPage = parseInt(page);
let fullUsername = event.params.handle;
if (!fullUsername) {
return json({ message: "Bad request" }, { status: 400 });
}
fullUsername = fullUsername.replace(/^@/, "");
const [username, domain] = splitUsername(fullUsername, env.ORIGIN)
const actor: Actor = await event.locals.pb.collection("activitypub_actors").getFirstListItem(`username:lower='${username?.toLowerCase()}'&&isLocal=true`)
const followers: ListResult<Follow> = await event.locals.pb.collection("follows").getList(intPage, 10, { sort: "-created", filter: `follower='${actor.id}'&&status='accepted'`, expand: "followee" })
const id = actor.iri;
const hasNextPage = intPage * 10 < followers.totalItems;
const following: APRoot<APOrderedCollectionPage> = {
"@context": [
"https://www.w3.org/ns/activitystreams",
],
type: "OrderedCollectionPage",
first: id + "/following?page=1",
...(intPage > 1 ? { prev: `${id}/following?page=${intPage - 1}` } : {}),
...(hasNextPage ? { next: `${id}/following?page=${intPage + 1}` } : {}),
partOf: id + "/following",
totalItems: followers.totalItems,
orderedItems: followers.items.map<string>(f => f.expand!.followee.iri)
}
const headers = new Headers()
headers.append("Content-Type", "application/activity+json")
return json(following, { headers });
} catch (e) {
return handleError(e)
}
}

View File

@@ -0,0 +1,43 @@
import { env as publicEnv } from '$env/dynamic/public';
import { handleError } from '$lib/util/api_util';
import { json, type RequestEvent } from '@sveltejs/kit';
import type { APActivity } from 'activitypub-types';
export async function POST(event: RequestEvent) {
try {
const activity: APActivity = await event.request.json()
if (!activity.actor) {
return json("Bad request", { status: 400 });
}
const success = await event.locals.pb.send("/activitypub/activity/process", {
method: "POST",
fetch: event.fetch,
headers: {
'X-Forwarded-Path': event.url.pathname,
'Content-Type': event.request.headers.get("content-type")!,
signature: event.request.headers.get("signature")!,
date: event.request.headers.get("date")!,
digest: event.request.headers.get("digest")!
},
body: JSON.stringify(activity)
})
if (success === false) {
return json("Invalid header signature", { status: 400 });
}
const headers = new Headers()
headers.append("Content-Type", "application/activity+json")
return json("", { status: 200, headers });
} catch (e) {
return handleError(e)
}
}

View File

@@ -0,0 +1,71 @@
import { env } from '$env/dynamic/private';
import { env as publicEnv } from '$env/dynamic/public';
import type { Activity } from '$lib/models/activitypub/activity';
import type { Actor } from '$lib/models/activitypub/actor';
import { RecordListOptionsSchema } from '$lib/models/api/base_schema';
import { splitUsername } from '$lib/util/activitypub_util';
import { handleError } from '$lib/util/api_util';
import { error, json, type RequestEvent } from '@sveltejs/kit';
import type { APActivity, APOrderedCollectionPage, APRoot } from 'activitypub-types';
import type { ListResult } from 'pocketbase';
export async function GET(event: RequestEvent) {
try {
const safeSearchParams = RecordListOptionsSchema.parse(Object.fromEntries(event.url.searchParams));
const page = safeSearchParams.page ?? 1
const perPage = safeSearchParams.perPage ?? 10
let fullUsername = event.params.handle;
if (!fullUsername) {
return json({ message: "Bad request" }, { status: 400 });
}
const [username, domain] = splitUsername(fullUsername, env.ORIGIN)
const actor: Actor = await event.locals.pb.collection("activitypub_actors").getFirstListItem(`username:lower='${username?.toLowerCase()}'&&isLocal=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 })
const id = actor.iri;
const hasNextPage = page * perPage < activities.totalItems;
const outbox: APRoot<APOrderedCollectionPage> = {
"@context": [
"https://www.w3.org/ns/activitystreams",
],
type: "OrderedCollectionPage",
first: id + "/outbox?page=1",
...(page > 1 ? { prev: `${id}/outbox?page=${page - 1}` } : {}),
...(hasNextPage ? { next: `${id}/outbox?page=${page + 1}` } : {}),
partOf: id + "/outbox",
totalItems: activities.totalItems,
orderedItems: activities.items.map<APActivity>(a => ({
id: a.iri,
actor: a.actor,
type: a.type,
to: a.to,
cc: a.cc,
published: a.published,
object: a.object
}))
}
const headers = new Headers()
headers.append("Content-Type", "application/activity+json")
return json(outbox, { headers });
} catch (e) {
return handleError(e)
}
}

View File

@@ -13,7 +13,7 @@ export async function POST(event: RequestEvent) {
const r = await event.locals.pb.collection('users').confirmPasswordReset(safeData.token, safeData.password, safeData.passwordConfirm);
return json(r);
} catch (e: any) {
throw handleError(e);
return handleError(e);
}
}

View File

@@ -15,7 +15,7 @@ export async function POST(event: RequestEvent) {
const r = await event.locals.pb.collection('users').authWithPassword(safeData.email ?? safeData.username!, data.password);
return json(r);
} catch (e: any) {
throw handleError(e);
return handleError(e);
}
}

View File

@@ -18,7 +18,7 @@ export async function GET(event: RequestEvent) {
}
return json(r)
} catch (e: any) {
throw handleError(e);
return handleError(e);
}
}
@@ -40,7 +40,7 @@ export async function POST(event: RequestEvent) {
)
return json(r)
} catch (e: any) {
throw handleError(e);
return handleError(e);
}
}

View File

@@ -12,7 +12,7 @@ export async function POST(event: RequestEvent) {
const r = await event.locals.pb.collection('users').requestPasswordReset(safeData.email);
return json(r);
} catch (e: any) {
throw handleError(e);
return handleError(e);
}
}

View File

@@ -7,7 +7,7 @@ export async function GET(event: RequestEvent) {
const r = await list<Category>(event, Collection.categories);
return json(r)
} catch (e) {
throw handleError(e)
return handleError(e)
}
}

View File

@@ -2,20 +2,70 @@ import { CommentCreateSchema } from '$lib/models/api/comment_schema';
import type { Comment } from '$lib/models/comment';
import { Collection, create, handleError, list } from '$lib/util/api_util';
import { json, type RequestEvent } from '@sveltejs/kit';
import { type ListResult } from "pocketbase";
export async function GET(event: RequestEvent) {
try {
const r = await list<Comment>(event, Collection.comments);
for (const comment of r.items) {
if (!comment.expand?.author) {
comment.expand = {
author: await event.locals.pb.collection('users_anonymous').getOne(comment.author)
}
if (!event.url.searchParams.has("handle")) {
const comments = await list<Comment>(event, Collection.comments);
return json(comments)
} else {
const { actor, error } = await event.locals.pb.send(`/activitypub/actor?resource=acct:${event.url.searchParams.get("handle")}`, { method: "GET", fetch: event.fetch, });
event.url.searchParams.delete("handle")
const localComments = await list<Comment>(event, Collection.comments);
if (actor.isLocal) {
return json(localComments)
}
const deduplicationMap: Record<string, Comment> = {}
localComments.items.forEach(c => {
if (c.iri) {
const id = c.iri.substring(c.iri.length - 15)
deduplicationMap[id] = c
} else if (c.id) {
deduplicationMap[c.id] = c
}
})
const origin = new URL(actor.iri).origin
const url = `${origin}/api/v1/comment`
const response = await event.fetch(url + '?' + event.url.searchParams, { method: 'GET' })
if (!response.ok) {
const errorResponse = await response.json()
console.error(errorResponse)
}
const remoteComments: ListResult<Comment> = await response.json()
remoteComments.items = remoteComments.items.filter(c => {
const iriId = c.iri?.substring(c.iri.length - 15) ?? ""
if (deduplicationMap[c.id!] != undefined) {
deduplicationMap[c.id!] = {...c, author: deduplicationMap[c.id!].author}
return false
} else if (deduplicationMap[iriId] != undefined) {
deduplicationMap[iriId] = {...c, author: deduplicationMap[iriId].author}
return false
}
return true
})
const allCommentItems = <ListResult<Comment>>{
items: localComments.items.concat(remoteComments.items),
page: localComments.page,
perPage: localComments.perPage,
totalItems: localComments.items.length + remoteComments.items.length,
totalPages: Math.ceil((localComments.items.length + remoteComments.items.length) / localComments.perPage)
}
allCommentItems.items = allCommentItems.items.sort((a, b) => {
return new Date(b.created ?? 0).getTime() - new Date(a.created ?? 0).getTime()
})
return json(allCommentItems)
}
return json(r)
} catch (e) {
throw handleError(e)
return handleError(e)
}
}
@@ -24,6 +74,6 @@ export async function PUT(event: RequestEvent) {
const r = await create<Comment>(event, CommentCreateSchema, Collection.comments)
return json(r);
} catch (e) {
throw handleError(e)
return handleError(e)
}
}

View File

@@ -6,14 +6,9 @@ import { json, type RequestEvent } from "@sveltejs/kit";
export async function GET(event: RequestEvent) {
try {
const r = await show<Comment>(event, Collection.comments)
if (!r.expand) {
r.expand = {} as any
}
r.expand!.author = await event.locals.pb.collection('users_anonymous').getOne(r.author)
return json(r)
} catch (e: any) {
throw handleError(e)
return handleError(e)
}
}
@@ -22,7 +17,7 @@ export async function POST(event: RequestEvent) {
const r = await update<Comment>(event, CommentUpdateSchema, Collection.comments)
return json(r);
} catch (e: any) {
throw handleError(e)
return handleError(e)
}
}
@@ -31,6 +26,6 @@ export async function DELETE(event: RequestEvent) {
const r = await remove(event, Collection.comments)
return json(r);
} catch (e: any) {
throw handleError(e)
return handleError(e)
}
}

View File

@@ -1,4 +1,4 @@
import { error, type RequestEvent } from "@sveltejs/kit";
import { error, json, type RequestEvent } from "@sveltejs/kit";
import { z } from "zod";
export async function GET(event: RequestEvent) {
@@ -10,7 +10,15 @@ export async function GET(event: RequestEvent) {
}).safeParse(event.params)
if (!safeParams.success) {
throw error(400, safeParams.error)
throw json({ message: safeParams.error }, { status: 400 })
}
const safeSearchParams = z.object({
thumb: z.string().regex(/[0-9]*x[0-9]*[tbf]?/).optional()
}).safeParse(Object.fromEntries(event.url.searchParams))
if (!safeSearchParams.success) {
throw json({ message: safeSearchParams.error }, { status: 400 })
}
const parts = [];
@@ -20,7 +28,7 @@ export async function GET(event: RequestEvent) {
parts.push(encodeURIComponent(safeParams.data.record));
parts.push(encodeURIComponent(safeParams.data.file));
let fileURL = event.locals.pb.buildURL(parts.join("/"));
let fileURL = event.locals.pb.buildURL(parts.join("/") + '?' + new URLSearchParams(safeSearchParams.data));
try {
const r = await event.fetch(fileURL)

View File

@@ -1,30 +1,78 @@
import type { Actor } from '$lib/models/activitypub/actor';
import { FollowCreateSchema } from '$lib/models/api/follow_schema';
import type { Follow } from '$lib/models/follow';
import type { UserAnonymous } from '$lib/models/user';
import { Collection, create, handleError, list } from '$lib/util/api_util';
import { APIError, Collection, handleError, list } from '$lib/util/api_util';
import { json, type RequestEvent } from '@sveltejs/kit';
import type { APOrderedCollectionPage } from 'activitypub-types';
import { ClientResponseError, type ListResult } from "pocketbase";
export async function GET(event: RequestEvent) {
try {
const r = await list<Follow>(event, Collection.follows);
for (const follow of r.items) {
const follower = await event.locals.pb.collection('users_anonymous').getOne<UserAnonymous>(follow.follower, { requestKey: null })
const followee = await event.locals.pb.collection('users_anonymous').getOne<UserAnonymous>(follow.followee, { requestKey: null })
follow.expand = {
follower, followee
if (!event.url.searchParams.has("handle")) {
const follows = await list<Follow>(event, Collection.follows);
return json(follows)
} else {
const handle = event.url.searchParams.get("handle");
const type = event.url.searchParams.get("type");
if (!handle || (type !== "followers" && type !== "following")) {
throw new APIError(400, "invalid params")
}
const { actor }: { actor: Actor } = await event.locals.pb.send(`/activitypub/actor?resource=acct:${handle}`, { method: "GET", fetch: event.fetch, });
const page = event.url.searchParams.get("page") ?? "1"
const headers = new Headers()
headers.set("Accept", 'application/ld+json')
const r = await event.fetch(actor[type as "followers" | "following"]! + '?' + new URLSearchParams({ page }), { headers })
if (!r.ok) {
const errorResponse = await r.json()
throw new ClientResponseError({ status: r.status, response: errorResponse });
}
const followers: APOrderedCollectionPage = await r.json()
const followerActors: Actor[] = []
for (const f of followers.orderedItems ?? []) {
try {
const { actor }: { actor: Actor } = await event.locals.pb.send(`/activitypub/actor?iri=${f}`, { method: "GET", fetch: event.fetch, });
followerActors.push(actor)
} catch (e) {
continue
}
}
const result: ListResult<Actor> = {
items: followerActors,
page: parseInt(page),
perPage: 10,
totalItems: actor.followerCount ?? 0,
totalPages: Math.ceil((actor.followerCount ?? 0) / 10)
}
return json(result)
}
return json(r)
} catch (e) {
throw handleError(e)
return handleError(e)
}
}
export async function PUT(event: RequestEvent) {
try {
const r = await create<Follow>(event, FollowCreateSchema, Collection.follows)
return json(r);
const data = await event.request.json();
const safeData = FollowCreateSchema.parse(data);
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" })
return json(follow);
} catch (e) {
throw handleError(e)
return handleError(e)
}
}

View File

@@ -7,7 +7,7 @@ export async function DELETE(event: RequestEvent) {
const r = await remove(event, Collection.follows)
return json(r);
} catch (e: any) {
throw handleError(e)
return handleError(e)
}
}

View File

@@ -1,11 +0,0 @@
import { Collection, handleError, show } from "$lib/util/api_util";
import { json, type RequestEvent } from "@sveltejs/kit";
export async function GET(event: RequestEvent) {
try {
const r = await show(event, Collection.follow_counts)
return json(r)
} catch (e: any) {
throw handleError(e)
}
}

View File

@@ -9,7 +9,7 @@ export async function GET(event: RequestEvent) {
return json(r)
} catch (e) {
throw handleError(e)
return handleError(e)
}
}
@@ -18,6 +18,6 @@ export async function PUT(event: RequestEvent) {
const r = await create<Comment>(event, IntegrationCreateSchema, Collection.integrations)
return json(r);
} catch (e) {
throw handleError(e)
return handleError(e)
}
}

View File

@@ -8,7 +8,7 @@ export async function GET(event: RequestEvent) {
const r = await show<Integration>(event, Collection.integrations)
return json(r)
} catch (e: any) {
throw handleError(e)
return handleError(e)
}
}
@@ -17,7 +17,7 @@ export async function POST(event: RequestEvent) {
const r = await update<Integration>(event, IntegrationUpdateSchema, Collection.integrations)
return json(r);
} catch (e: any) {
throw handleError(e)
return handleError(e)
}
}
@@ -26,6 +26,6 @@ export async function DELETE(event: RequestEvent) {
const r = await remove(event, Collection.integrations)
return json(r);
} catch (e: any) {
throw handleError(e)
return handleError(e)
}
}

View File

@@ -8,6 +8,6 @@ export async function GET(event: RequestEvent) {
});
return json(r);
} catch (e: any) {
throw handleError(e)
return handleError(e)
}
}

View File

@@ -1,30 +1,30 @@
import type { Actor } from '$lib/models/activitypub/actor';
import { ListShareCreateSchema } from '$lib/models/api/list_share_schema';
import type { ListShare } from '$lib/models/list_share';
import type { User } from '$lib/models/user';
import { Collection, create, handleError, list } from '$lib/util/api_util';
import { json, type RequestEvent } from '@sveltejs/kit';
export async function GET(event: RequestEvent) {
try {
const r = await list<ListShare>(event, Collection.list_share);
for (const share of r.items) {
const anonymous_user = await event.locals.pb.collection('users_anonymous').getOne<User>(share.user)
share.expand = {
user: anonymous_user
}
}
return json(r)
} catch (e: any) {
throw handleError(e);
return handleError(e);
}
}
export async function PUT(event: RequestEvent) {
try {
const r = await create<ListShare>(event, ListShareCreateSchema, Collection.list_share)
const data = await event.request.json();
const safeData = ListShareCreateSchema.parse(data);
const { actor }: { actor: Actor } = await event.locals.pb.send(`/activitypub/actor?iri=${safeData.actor}`, { method: "GET", fetch: event.fetch, });
safeData.actor = actor.id!;
const r = await event.locals.pb.collection(Collection.list_share).create<ListShare>(safeData)
return json(r);
} catch (e) {
throw handleError(e)
return handleError(e)
}
}

View File

@@ -8,7 +8,7 @@ export async function GET(event: RequestEvent) {
const r = await show<ListShare>(event, Collection.list_share)
return json(r)
} catch (e: any) {
throw handleError(e)
return handleError(e)
}
}
@@ -17,7 +17,7 @@ export async function POST(event: RequestEvent) {
const r = await update<ListShare>(event, ListShareUpdateSchema, Collection.list_share)
return json(r);
} catch (e: any) {
throw handleError(e)
return handleError(e)
}
}
@@ -26,6 +26,6 @@ export async function DELETE(event: RequestEvent) {
const r = await remove(event, Collection.list_share)
return json(r);
} catch (e: any) {
throw handleError(e)
return handleError(e)
}
}

View File

@@ -10,14 +10,11 @@ export async function GET(event: RequestEvent) {
if (!t.author || !event.locals.pb.authStore.record) {
continue;
}
if (!t.expand) {
t.expand = {}
}
t.expand!.author = await event.locals.pb.collection("users_anonymous").getOne(t.author);
}
return json(r)
} catch (e) {
throw handleError(e)
return handleError(e)
}
}
@@ -26,6 +23,6 @@ export async function PUT(event: RequestEvent) {
const r = await create<List>(event, ListCreateSchema, Collection.lists)
return json(r);
} catch (e) {
throw handleError(e)
return handleError(e)
}
}

View File

@@ -1,19 +1,82 @@
import { ListUpdateSchema } from "$lib/models/api/list_schema";
import type { List } from "$lib/models/list";
import { Collection, handleError, remove, show, update } from "$lib/util/api_util";
import { objectToFormData } from "$lib/util/file_util";
import { json, type RequestEvent } from "@sveltejs/kit";
import { ClientResponseError } from "pocketbase";
export async function GET(event: RequestEvent) {
try {
const r = await show<List>(event, Collection.lists)
if (!r.expand) {
r.expand = {} as any
}
r.expand!.author = await event.locals.pb.collection('users_anonymous').getOne(r.author!)
if (!event.url.searchParams.has("handle")) {
const l = await show<List>(event, Collection.lists)
return json(l)
} else {
const {actor, error} = await event.locals.pb.send(`/activitypub/actor?resource=acct:${event.url.searchParams.get("handle")}`, { method: "GET", fetch: event.fetch, });
event.url.searchParams.delete("handle")
const origin = new URL(actor.iri).origin
const url = `${origin}/api/v1/list/${event.params.id}`
let dbList: List | undefined;
try {
dbList = await event.locals.pb.collection("lists").getFirstListItem(`iri='${url}'||id='${event.params.id}'`, {
...Object.fromEntries(event.url.searchParams)
})
} catch (e) {
if (!(e instanceof ClientResponseError) || e.status != 404) {
throw e
}
}
if (actor.isLocal) {
return json(dbList)
} else {
const response = await event.fetch((dbList?.iri ?? url) + '?' + event.url.searchParams, { method: 'GET' })
if (!response.ok) {
const errorResponse = await response.json()
console.error(errorResponse)
const cachedList = await event.locals.pb.collection("lists").getOne(`${event.params.id}`)
return json(cachedList)
}
const l: List = await response.json()
l.avatar = l.avatar ? `${origin}/api/v1/files/lists/${l.id}/${l.avatar}` : undefined
l.author = actor.id!
l.expand!.author = actor
l.iri = dbList?.iri ?? url;
l.expand?.trails?.forEach(t => {
t.gpx = t.gpx ? `${origin}/api/v1/files/trails/${t.id}/${t.gpx}` : undefined
t.photos = t.photos.map(p =>
`${origin}/api/v1/files/trails/${t.id}/${p}`
)
t.iri = t.iri || `${origin}/api/v1/trails/${t.id}`;
})
const formData = objectToFormData({ ...l, id: dbList?.id, expand: undefined, trails: [] })
if (l.avatar) {
const avatarURL = l.avatar
let response = await event.fetch(avatarURL, { method: "GET" })
const avatar = await response.blob()
formData.append("avatar", avatar)
}
if (dbList !== undefined) {
dbList = await event.locals.pb.collection("lists").update(dbList.id!, formData)
} else {
dbList = await event.locals.pb.collection("lists").create(formData)
}
l.id = dbList!.id
return json(l)
}
}
return json(r)
} catch (e: any) {
throw handleError(e)
return handleError(e)
}
}
@@ -22,7 +85,7 @@ export async function POST(event: RequestEvent) {
const r = await update<List>(event, ListUpdateSchema, Collection.lists)
return json(r);
} catch (e: any) {
throw handleError(e)
return handleError(e)
}
}
@@ -31,7 +94,7 @@ export async function DELETE(event: RequestEvent) {
const r = await remove(event, Collection.lists)
return json(r);
} catch (e: any) {
throw handleError(e)
return handleError(e)
}
}

View File

@@ -7,6 +7,6 @@ export async function POST(event: RequestEvent) {
const r = await upload<List>(event, Collection.lists);
return json(r);
} catch (e: any) {
throw handleError(e)
return handleError(e)
}
}

View File

@@ -0,0 +1,12 @@
import type { List } from '$lib/models/list';
import { Collection, handleError, uploadCreate } from '$lib/util/api_util';
import { json, type RequestEvent } from '@sveltejs/kit';
export async function PUT(event: RequestEvent) {
try {
const r = await uploadCreate<List>(event, Collection.lists)
return json(r);
} catch (e) {
return handleError(e)
}
}

View File

@@ -0,0 +1,12 @@
import type { List } from "$lib/models/list";
import { Collection, handleError, uploadUpdate } from "$lib/util/api_util";
import { json, type RequestEvent } from "@sveltejs/kit";
export async function POST(event: RequestEvent) {
try {
const r = await uploadUpdate<List>(event, Collection.lists)
return json(r);
} catch (e) {
return handleError(e)
}
}

View File

@@ -1,5 +1,4 @@
import type { Notification } from '$lib/models/notification';
import type { UserAnonymous } from '$lib/models/user';
import { Collection, handleError, list } from '$lib/util/api_util';
import { json, type RequestEvent } from '@sveltejs/kit';
@@ -7,15 +6,8 @@ export async function GET(event: RequestEvent) {
try {
const r = await list<Notification>(event, Collection.notifications);
for (const notification of r.items) {
const recipient = await event.locals.pb.collection('users_anonymous').getOne<UserAnonymous>(notification.recipient, { requestKey: null })
const author = await event.locals.pb.collection('users_anonymous').getOne<UserAnonymous>(notification.author, { requestKey: null })
notification.expand = {
recipient, author
}
}
return json(r)
} catch (e: any) {
throw handleError(e);
return handleError(e);
}
}

View File

@@ -8,6 +8,6 @@ export async function POST(event: RequestEvent) {
const r = await update<Comment>(event, NotificationUpdateSchema, Collection.notifications)
return json(r);
} catch (e: any) {
throw handleError(e)
return handleError(e)
}
}

View File

@@ -0,0 +1,35 @@
import type { Profile } from '$lib/models/profile';
import { handleError } from '$lib/util/api_util';
import { error, json, type RequestEvent } from '@sveltejs/kit';
export async function GET(event: RequestEvent) {
const handle = event.params.handle;
if (!handle) {
return error(400, { message: "Bad request" })
}
try {
const { actor, error } = await event.locals.pb.send(`/activitypub/actor?resource=acct:${handle}&follows=true`, { method: "GET", fetch: event.fetch, });
const profile: Profile = {
id: actor.id!,
username: actor.username,
preferredUsername: actor.preferred_username,
acct: handle,
createdAt: actor.published ?? "",
bio: actor.summary ?? "",
uri: actor.iri,
followers: actor.followerCount ?? 0,
following: actor.followingCount ?? 0,
icon: actor.icon ?? "",
error
}
return json({ profile, actor: actor })
} catch (e) {
if (e instanceof Error && e.message == "fetch failed") {
return error(404, "Not found")
}
return handleError(e)
}
}

View File

@@ -0,0 +1,43 @@
import type { TrailSearchResult } from '$lib/models/trail';
import type { ListSearchResult } from '$lib/stores/search_store';
import { handleError } from '$lib/util/api_util';
import { error, json, type RequestEvent } from '@sveltejs/kit';
import type { SearchResponse } from 'meilisearch';
import { ClientResponseError } from 'pocketbase';
export async function POST(event: RequestEvent) {
const handle = event.params.handle;
if (!handle) {
return error(400, { message: "Bad request" })
}
try {
const {actor, error} = await event.locals.pb.send(`/activitypub/actor?resource=acct:${handle}`, { method: "GET", fetch: event.fetch, });
const data = await event.request.json()
let r: SearchResponse<ListSearchResult>;
if (actor.isLocal) {
r = await event.locals.ms.index("lists").search(data.q, { ...data.options, filter: `author = ${actor.id}` });
} else {
const origin = new URL(actor.iri).origin
const url = `${origin}/api/v1/profile/${actor.username}/lists?` + event.url.searchParams
const response = await event.fetch(url, { method: 'POST', body: JSON.stringify(data) })
if (!response.ok) {
const errorResponse = await response.json()
throw new ClientResponseError({ status: response.status, response: errorResponse });
}
r = await response.json()
r.hits.forEach(h => {
h.avatar = h.avatar ? `${origin}/api/v1/files/lists/${h.id}/${h.avatar}` : undefined;
})
}
return json(r)
} catch (e) {
return handleError(e)
}
}

View File

@@ -0,0 +1,49 @@
import { RecordListOptionsSchema } from '$lib/models/api/base_schema';
import type { SummitLog } from '$lib/models/summit_log';
import { Collection, handleError } from '$lib/util/api_util';
import { error, json, type RequestEvent } from '@sveltejs/kit';
import { ClientResponseError, type ListResult } from 'pocketbase';
export async function GET(event: RequestEvent) {
const handle = event.params.handle;
if (!handle) {
return error(400, { message: "Bad request" })
}
try {
const {actor, error} = await event.locals.pb.send(`/activitypub/actor?resource=acct:${handle}`, { method: "GET", fetch: event.fetch, });
const searchParams = Object.fromEntries(event.url.searchParams);
const safeSearchParams = RecordListOptionsSchema.parse(searchParams);
let summitLogs: ListResult<SummitLog>;
if (actor.isLocal) {
summitLogs = await event.locals.pb.collection(Collection.summit_logs)
.getList<SummitLog>(safeSearchParams.page, safeSearchParams.perPage, { ...safeSearchParams, filter: `author='${actor.id}'` })
} else {
const origin = new URL(actor.iri).origin
const summitLogURL = `${origin}/api/v1/profile/${actor.username}/stats?` + event.url.searchParams
const response = await event.fetch(summitLogURL, { method: 'GET' })
if (!response.ok) {
const errorResponse = await response.json()
throw new ClientResponseError({ status: response.status, response: errorResponse });
}
summitLogs = await response.json()
summitLogs.items.forEach(i => {
i.photos = i.photos.map(p =>
`${origin}/api/v1/files/summit_logs/${i.id}/${p}`
)
if (i.gpx) {
i.gpx = `${origin}/api/v1/files/summit_logs/${i.id}/${i.gpx}`
}
})
}
return json(summitLogs)
} catch (e) {
return handleError(e)
}
}

View File

@@ -0,0 +1,48 @@
import type { Actor } from '$lib/models/activitypub/actor';
import { RecordListOptionsSchema } from '$lib/models/api/base_schema';
import { type TimelineItem } from '$lib/models/timeline';
import { Collection, handleError } from '$lib/util/api_util';
import { error, json, type RequestEvent } from '@sveltejs/kit';
import { ClientResponseError, type ListResult } from 'pocketbase';
export async function GET(event: RequestEvent) {
const handle = event.params.handle;
if (!handle) {
return error(400, { message: "Bad request" })
}
try {
const {actor, error} = await event.locals.pb.send(`/activitypub/actor?resource=acct:${handle}`, { method: "GET", fetch: event.fetch, });
const searchParams = Object.fromEntries(event.url.searchParams);
const safeSearchParams = RecordListOptionsSchema.parse(searchParams);
let timeline: ListResult<TimelineItem>;
if (actor.isLocal) {
timeline = await event.locals.pb.collection(Collection.timeline)
.getList<TimelineItem>(safeSearchParams.page, safeSearchParams.perPage, { ...safeSearchParams, filter: `author='${actor.iri}'` })
} else {
const origin = new URL(actor.iri).origin
const timelineURL = `${origin}/api/v1/profile/${actor.username}/timeline?` + event.url.searchParams
const response = await event.fetch(timelineURL, { method: 'GET' })
if (!response.ok) {
const errorResponse = await response.json()
throw new ClientResponseError({ status: response.status, response: errorResponse });
}
timeline = await response.json()
timeline.items.forEach(i => {
i.photos = i. photos.map(p =>
`${origin}/api/v1/files/${i.type}s/${i.id}/${p}`
)
})
}
return json(timeline)
} catch (e) {
console.error(e)
return handleError(e)
}
}

View File

@@ -0,0 +1,43 @@
import type { TrailSearchResult } from '$lib/models/trail';
import { handleError } from '$lib/util/api_util';
import { error, json, type RequestEvent } from '@sveltejs/kit';
import type { SearchResponse } from 'meilisearch';
import { ClientResponseError } from 'pocketbase';
export async function POST(event: RequestEvent) {
const handle = event.params.handle;
if (!handle) {
return error(400, { message: "Bad request" })
}
try {
const {actor, error} = await event.locals.pb.send(`/activitypub/actor?resource=acct:${handle}`, { method: "GET", fetch: event.fetch, });
const data = await event.request.json()
let r: SearchResponse<TrailSearchResult>;
if (actor.isLocal) {
r = await event.locals.ms.index("trails").search(data.q, { ...data.options, filter: `author = ${actor.id}` });
} else {
const origin = new URL(actor.iri).origin
const url = `${origin}/api/v1/profile/${actor.username}/trails?` + event.url.searchParams
const response = await event.fetch(url, { method: 'POST', body: JSON.stringify(data) })
if (!response.ok) {
const errorResponse = await response.json()
throw new ClientResponseError({ status: response.status, response: errorResponse });
}
r = await response.json()
r.hits.forEach(h => {
h.thumbnail = `${origin}/api/v1/files/trails/${h.id}/${h.thumbnail}`;
h.domain = actor.domain
})
}
return json(r)
} catch (e) {
return handleError(e)
}
}

View File

@@ -4,11 +4,11 @@ export async function POST(event: RequestEvent) {
const data = await event.request.json()
try {
const r = await event.locals.ms.index(event.params.index as string).search(data.q, data.options);
const r = await event.locals.ms.index(event.params.index as string).search(data.q, data.options);
return json(r);
} catch (e: any) {
console.error(e);
throw error(e.httpStatus, e)
}
}

View File

@@ -0,0 +1,46 @@
import type { Actor } from '$lib/models/activitypub/actor';
import { 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"
export async function GET(event: RequestEvent) {
try {
if (!event.url.searchParams.has("q")) {
throw new ClientResponseError({ status: 400, response: "Bad request" });
}
const q = event.url.searchParams.get("q")
const [user, domain] = splitUsername(q!)
let filter = `username~'${user}'`;
if (event.url.searchParams.get("includeSelf") == "false" && event.locals.pb.authStore.record) {
filter += `&& id != "${event.locals.pb.authStore.record.actor}"`
}
const response = await event.locals.pb.collection("activitypub_actors").getList(1, 3, { filter: filter })
try {
const { actor, error } = await event.locals.pb.send(`/activitypub/actor?resource=acct:${q}&follows=false`, { method: "GET", fetch: event.fetch, });
if (!response.items.find(i => i.iri == actor.iri)) {
response.items.push(actor)
}
} catch (e) {
}
return json({ items: response.items })
} catch (e) {
if (e instanceof Error && e.message == "fetch failed") {
return error(404, "Not found")
}
return handleError(e)
}
}

View File

@@ -9,7 +9,7 @@ export async function GET(event: RequestEvent) {
const r = await show<Settings>(event, Collection.settings)
return json(r)
} catch (e: any) {
throw handleError(e);
return handleError(e);
}
}
@@ -18,7 +18,7 @@ export async function POST(event: RequestEvent) {
const r = await update<Settings>(event, SettingsCreateSchema, Collection.settings)
return json(r);
} catch (e: any) {
throw handleError(e);
return handleError(e);
}
}
@@ -27,6 +27,6 @@ export async function DELETE(event: RequestEvent) {
const r = await remove(event, Collection.settings)
return json(r);
} catch (e: any) {
throw handleError(e);
return handleError(e);
}
}

View File

@@ -2,14 +2,90 @@ import { SummitLogCreateSchema } from '$lib/models/api/summit_log_schema';
import type { SummitLog } from '$lib/models/summit_log';
import { Collection, create, handleError, list } from '$lib/util/api_util';
import { json, type RequestEvent } from '@sveltejs/kit';
import { type ListResult } from "pocketbase";
export async function GET(event: RequestEvent) {
try {
const r = await list<SummitLog>(event, Collection.summit_logs);
if (!event.url.searchParams.has("handle")) {
const summitLogs = await list<SummitLog>(event, Collection.summit_logs);
removeTimeFromDates(summitLogs.items)
return json(summitLogs)
} else {
const { actor, error } = await event.locals.pb.send(`/activitypub/actor?resource=acct:${event.url.searchParams.get("handle")}`, { method: "GET", fetch: event.fetch, });
event.url.searchParams.delete("handle")
const localSummitLogs = await list<SummitLog>(event, Collection.summit_logs);
if (actor.isLocal) {
removeTimeFromDates(localSummitLogs.items)
return json(r)
} catch (e: any) {
throw handleError(e);
return json(localSummitLogs)
}
const deduplicationMap: Record<string, SummitLog> = {}
localSummitLogs.items.forEach(l => {
if (l.iri) {
const id = l.iri.substring(l.iri.length - 15)
deduplicationMap[id] = l
} else if (l.id) {
deduplicationMap[l.id] = l
}
l.date = l.date.substring(0, 10);
})
const origin = new URL(actor.iri).origin
const url = `${origin}/api/v1/summit-log`
const response = await event.fetch(url + '?' + event.url.searchParams, { method: 'GET' })
if (!response.ok) {
const errorResponse = await response.json()
console.error(errorResponse)
}
const remoteSummitLogs: ListResult<SummitLog> = await response.json()
remoteSummitLogs.items = remoteSummitLogs.items.filter(l => {
const iriId = l.iri?.substring(l.iri.length - 15) ?? ""
if (deduplicationMap[l.id!] != undefined) {
deduplicationMap[l.id!] = {...l, author: deduplicationMap[l.id!].author}
return false
} else if (deduplicationMap[iriId] != undefined) {
deduplicationMap[iriId] = {...l, author: deduplicationMap[iriId].author}
return false
}
return true
})
remoteSummitLogs.items.forEach(l => {
if (l.gpx) {
l.gpx = `${origin}/api/v1/files/summit_logs/${l.id}/${l.gpx}`
}
l.photos = l.photos.map(p =>
`${origin}/api/v1/files/summit_logs/${l.id}/${p}`
)
if (l.expand?.author) {
l.expand.author.isLocal = false
}
})
const allSummitLogItems = <ListResult<SummitLog>>{
items: localSummitLogs.items.concat(remoteSummitLogs.items),
page: localSummitLogs.page,
perPage: localSummitLogs.perPage,
totalItems: localSummitLogs.items.length + remoteSummitLogs.items.length,
totalPages: Math.ceil((localSummitLogs.items.length + remoteSummitLogs.items.length) / localSummitLogs.perPage)
}
allSummitLogItems.items = allSummitLogItems.items.sort((a, b) => {
return new Date(a.created ?? 0).getTime() - new Date(b.created ?? 0).getTime()
})
return json(allSummitLogItems)
}
} catch (e) {
return handleError(e)
}
}
@@ -18,6 +94,12 @@ export async function PUT(event: RequestEvent) {
const r = await create<SummitLog>(event, SummitLogCreateSchema, Collection.summit_logs)
return json(r);
} catch (e: any) {
throw handleError(e)
return handleError(e)
}
}
function removeTimeFromDates(logs: SummitLog[]) {
logs.forEach(l => l.date = l.date.substring(0, 10));
}

View File

@@ -7,15 +7,9 @@ import { error, json, type RequestEvent } from "@sveltejs/kit";
export async function GET(event: RequestEvent) {
try {
const r = await show<SummitLog>(event, Collection.summit_logs)
if (!r.expand) {
r.expand = {} as any
}
r.expand!.author = await event.locals.pb.collection("users_anonymous").getOne(r.author!);
return json(r)
} catch (e: any) {
throw handleError(e);
return handleError(e);
}
}
@@ -24,7 +18,7 @@ export async function POST(event: RequestEvent) {
const r = await update<SummitLog>(event, SummitLogUpdateSchema, Collection.summit_logs)
return json(r);
} catch (e: any) {
throw handleError(e)
return handleError(e)
}
}
@@ -33,6 +27,6 @@ export async function DELETE(event: RequestEvent) {
const r = await remove(event, Collection.summit_logs)
return json(r);
} catch (e: any) {
throw handleError(e);
return handleError(e);
}
}

View File

@@ -0,0 +1,14 @@
import type { SummitLog } from '$lib/models/summit_log';
import { Collection, handleError, uploadCreate } from '$lib/util/api_util';
import { json, type RequestEvent } from '@sveltejs/kit';
export async function PUT(event: RequestEvent) {
try {
const r = await uploadCreate<SummitLog>(event, Collection.summit_logs)
r.date = r.date?.substring(0, 10) ?? "";
return json(r);
} catch (e) {
return handleError(e)
}
}

View File

@@ -0,0 +1,13 @@
import type { SummitLog } from "$lib/models/summit_log";
import { Collection, handleError, uploadUpdate } from "$lib/util/api_util";
import { json, type RequestEvent } from "@sveltejs/kit";
export async function POST(event: RequestEvent) {
try {
const r = await uploadUpdate<SummitLog>(event, Collection.summit_logs)
r.date = r.date?.substring(0, 10) ?? "";
return json(r);
} catch (e) {
return handleError(e)
}
}

View File

@@ -9,7 +9,7 @@ export async function GET(event: RequestEvent) {
return json(r)
} catch (e: any) {
throw handleError(e);
return handleError(e);
}
}
@@ -18,6 +18,6 @@ export async function PUT(event: RequestEvent) {
const r = await create<Tag>(event, TagCreateSchema, Collection.tags)
return json(r);
} catch (e) {
throw handleError(e)
return handleError(e)
}
}

View File

@@ -8,7 +8,7 @@ export async function GET(event: RequestEvent) {
const r = await show<Tag>(event, Collection.tags)
return json(r)
} catch (e: any) {
throw handleError(e)
return handleError(e)
}
}
@@ -17,7 +17,7 @@ export async function POST(event: RequestEvent) {
const r = await update<Tag>(event, TagUpdateSchema, Collection.tags)
return json(r);
} catch (e: any) {
throw handleError(e)
return handleError(e)
}
}
@@ -26,6 +26,6 @@ export async function DELETE(event: RequestEvent) {
const r = await remove(event, Collection.tags)
return json(r);
} catch (e: any) {
throw handleError(e)
return handleError(e)
}
}

View File

@@ -0,0 +1,23 @@
import { TrailLikeCreateSchema } from '$lib/models/api/trail_like_schema';
import type { TrailLike } from '$lib/models/trail_like';
import { Collection, create, handleError, list } from '$lib/util/api_util';
import { json, type RequestEvent } from '@sveltejs/kit';
export async function GET(event: RequestEvent) {
try {
const r = await list<TrailLike>(event, Collection.trail_share);
return json(r)
} catch (e: any) {
handleError(e)
}
}
export async function PUT(event: RequestEvent) {
try {
const r = await create<TrailLike>(event, TrailLikeCreateSchema, Collection.trail_like)
return json(r);
} catch (e) {
return handleError(e)
}
}

View File

@@ -0,0 +1,21 @@
import type { TrailLike } from "$lib/models/trail_like";
import { Collection, handleError, remove, show } from "$lib/util/api_util";
import { json, type RequestEvent } from "@sveltejs/kit";
export async function GET(event: RequestEvent) {
try {
const r = await show<TrailLike>(event, Collection.trail_like)
return json(r)
} catch (e: any) {
return handleError(e)
}
}
export async function DELETE(event: RequestEvent) {
try {
const r = await remove(event, Collection.trail_like)
return json(r);
} catch (e: any) {
return handleError(e)
}
}

View File

@@ -0,0 +1,21 @@
import { TrailLikeCreateSchema } from '$lib/models/api/trail_like_schema';
import { Collection, handleError } from '$lib/util/api_util';
import { json, type RequestEvent } from '@sveltejs/kit';
// this route exists so we can delete a like without knowing its ID
export async function POST(event: RequestEvent) {
try {
const data = await event.request.json();
const safeData = TrailLikeCreateSchema.parse(data);
const like = await event.locals.pb.collection(Collection.trail_like).getFirstListItem(`actor='${safeData.actor}'&&trail='${safeData.trail}'`)
const r = await event.locals.pb.collection(Collection.trail_like).delete(like.id)
return json({ 'acknowledged': r })
} catch (e: any) {
handleError(e)
}
}

View File

@@ -1,18 +1,12 @@
import type { Actor } from '$lib/models/activitypub/actor';
import { TrailShareCreateSchema } from '$lib/models/api/trail_share_schema';
import type { TrailShare } from '$lib/models/trail_share';
import type { UserAnonymous } from '$lib/models/user';
import { Collection, handleError, list, create } from '$lib/util/api_util';
import { Collection, create, handleError, list } from '$lib/util/api_util';
import { json, type RequestEvent } from '@sveltejs/kit';
export async function GET(event: RequestEvent) {
try {
const r = await list<TrailShare>(event, Collection.trail_share);
for (const share of r.items) {
const anonymous_user = await event.locals.pb.collection('users_anonymous').getOne<UserAnonymous>(share.user)
share.expand = {
user: anonymous_user
}
}
return json(r)
} catch (e: any) {
handleError(e)
@@ -21,9 +15,16 @@ export async function GET(event: RequestEvent) {
export async function PUT(event: RequestEvent) {
try {
const r = await create<TrailShare>(event, TrailShareCreateSchema, Collection.trail_share)
const data = await event.request.json();
const safeData = TrailShareCreateSchema.parse(data);
const { actor }: { actor: Actor } = await event.locals.pb.send(`/activitypub/actor?iri=${safeData.actor}`, { method: "GET", fetch: event.fetch, });
safeData.actor = actor.id!;
const r = await event.locals.pb.collection(Collection.trail_share).create<TrailShare>(safeData)
return json(r);
} catch (e) {
throw handleError(e)
return handleError(e)
}
}

View File

@@ -8,7 +8,7 @@ export async function GET(event: RequestEvent) {
const r = await show<TrailShare>(event, Collection.trail_share)
return json(r)
} catch (e: any) {
throw handleError(e)
return handleError(e)
}
}
@@ -17,7 +17,7 @@ export async function POST(event: RequestEvent) {
const r = await update<TrailShare>(event, TrailShareUpdateSchema, Collection.trail_share)
return json(r);
} catch (e: any) {
throw handleError(e)
return handleError(e)
}
}
@@ -26,6 +26,6 @@ export async function DELETE(event: RequestEvent) {
const r = await remove(event, Collection.trail_share)
return json(r);
} catch (e: any) {
throw handleError(e)
return handleError(e)
}
}

View File

@@ -19,23 +19,23 @@ export async function GET(event: RequestEvent) {
}
return json(r)
} catch (e: any) {
throw handleError(e);
return handleError(e);
}
}
export async function PUT(event: RequestEvent) {
try {
try {
const r = await create<Trail>(event, TrailCreateSchema, Collection.trails)
enrichRecord(r);
return json(r);
} catch (e) {
throw handleError(e)
return handleError(e)
}
}
function enrichRecord(r: Trail) {
r.date = r.date?.substring(0, 10) ?? "";
for (const log of r.expand?.summit_logs ?? []) {
for (const log of r.expand?.summit_logs_via_trail ?? []) {
log.date = log.date.substring(0, 10);
}
}

View File

@@ -1,28 +1,137 @@
import type { Trail } from "$lib/models/trail";
import { Collection, handleError, remove, show, update } from "$lib/util/api_util";
import { error, json, type RequestEvent } from "@sveltejs/kit";
import { RecordOptionsSchema } from '$lib/models/api/base_schema';
import { TrailUpdateSchema } from '$lib/models/api/trail_schema';
import type { Trail } from "$lib/models/trail";
import { APIError, Collection, handleError, remove, show, update } from "$lib/util/api_util";
import { objectToFormData } from "$lib/util/file_util";
import { json, type RequestEvent } from "@sveltejs/kit";
import type PocketBase from "pocketbase";
import { ClientResponseError } from "pocketbase";
export async function GET(event: RequestEvent) {
try {
const r = await show<Trail>(event, Collection.trails)
// try to get the trail simply via the
let t: Trail;
if (!event.url.searchParams.has("handle")) {
t = await show<Trail>(event, Collection.trails)
} else {
let { actor, error } = await event.locals.pb.send(`/activitypub/actor?resource=acct:${event.url.searchParams.get("handle")}`, { method: "GET", fetch: event.fetch, });
event.url.searchParams.delete("handle")
if (event.locals.pb.authStore.record) {
if (!r.expand) {
r.expand = {} as any
const safeSearchParams = RecordOptionsSchema.parse(Object.fromEntries(event.url.searchParams));
let origin = new URL(actor.iri).origin
let iri = `${origin}/api/v1/trail/${event.params.id}`
try {
t = await event.locals.pb.collection("trails").getFirstListItem(`iri='${iri}'||id='${event.params.id}'`, {
...safeSearchParams
})
} catch (e) {
if (!(e instanceof ClientResponseError) || e.status != 404) {
throw e
}
t = {
iri: iri,
author: actor.id
} as Trail
}
}
if (t.iri) {
const origin = new URL(t.iri).origin
const actor = await event.locals.pb.collection("activitypub_actors").getOne(t.author)
const localTrailId = t.id;
const localTrailIRI = t.iri;
const localLikeCount = t.like_count;
const localLikes = t.expand?.trail_like_via_trail;
const response = await event.fetch((t.iri) + '?' + event.url.searchParams, { method: 'GET' })
if (!response.ok) {
const errorResponse = await response.json()
console.error(errorResponse)
if (t.id) {
return json(t)
} else {
throw new ClientResponseError({ status: response.status, response: errorResponse })
}
}
t = await response.json()
// this came directly from the database of the remote instance
// we need to adjust some urls to get photos, gpx etc.
if (!t.iri) {
if (t.gpx) {
t.gpx = `${origin}/api/v1/files/trails/${t.id}/${t.gpx}`
}
t.photos = t.photos.map(p =>
`${origin}/api/v1/files/trails/${t.id}/${p}`
)
t.expand?.summit_logs_via_trail?.forEach(l => {
if (l.gpx) {
l.gpx = `${origin}/api/v1/files/summit_logs/${l.id}/${l.gpx}`
}
l.photos = l.photos.map(p =>
`${origin}/api/v1/files/summit_logs/${l.id}/${p}`
)
if (l.expand?.author) {
l.expand.author.isLocal = false
}
})
t.expand?.waypoints?.forEach(w => {
w.photos = w.photos.map(p =>
`${origin}/api/v1/files/waypoints/${w.id}/${p}`
)
})
t.author = actor.id!
t.expand!.author = actor as any
t.id = localTrailId
t.iri = localTrailIRI
t.like_count = localLikeCount
t.expand!.trail_like_via_trail = localLikes;
}
let categoryId: string | undefined;
try {
const category = await event.locals.pb.collection("categories").getFirstListItem(`name='${t.expand?.category?.name}'`)
categoryId = category.id;
t.expand!.category = category as any
} catch (e) { }
const formData = objectToFormData({ ...t, id: t.id, gpx: undefined, expand: undefined, photos: [], waypoints: [], tags: [], category: categoryId })
if (t.photos.length) {
const photoURL = t.photos[t.thumbnail ?? 0]
let response = await event.fetch(photoURL, { method: "GET" })
const photo = await response.blob()
formData.append("photos", photo)
}
if (t.gpx) {
const gpxURL = t.gpx
const response = await event.fetch(gpxURL, { method: "GET" })
const gpx = await response.blob()
formData.append("gpx", gpx)
}
if (t.id) {
await event.locals.pb.collection("trails").update(t.id, formData)
} else {
const createdTrail = await event.locals.pb.collection("trails").create(formData)
t.id = createdTrail.id;
}
r.expand!.author = await event.locals.pb.collection("users_anonymous").getOne(r.author!);
}
// remove time from dates
await enrichRecord(event.locals.pb, r);
await enrichRecord(event.locals.pb, t);
// sort waypoints by distance
r.expand?.waypoints?.sort((a, b) => (a.distance_from_start ?? 0) - (b.distance_from_start ?? 0))
return json(r)
t.expand?.waypoints?.sort((a, b) => (a.distance_from_start ?? 0) - (b.distance_from_start ?? 0))
return json(t)
} catch (e: any) {
throw handleError(e)
return handleError(e)
}
}
@@ -32,7 +141,7 @@ export async function POST(event: RequestEvent) {
await enrichRecord(event.locals.pb, r)
return json(r);
} catch (e: any) {
throw handleError(e)
return handleError(e)
}
}
@@ -41,7 +150,7 @@ export async function DELETE(event: RequestEvent) {
const r = await remove(event, Collection.trails)
return json(r);
} catch (e: any) {
throw handleError(e)
return handleError(e)
}
}
@@ -49,14 +158,7 @@ export async function DELETE(event: RequestEvent) {
async function enrichRecord(pb: PocketBase, r: Trail) {
r.date = r.date?.substring(0, 10) ?? "";
for (const log of r.expand?.summit_logs ?? []) {
for (const log of r.expand?.summit_logs_via_trail ?? []) {
log.date = log.date.substring(0, 10);
if (!log.expand) {
log.expand = {} as any;
}
if (log.author) {
log.expand!.author = await pb.collection("users_anonymous").getOne(log.author);
}
}
}

View File

@@ -12,9 +12,9 @@ export async function GET(event: RequestEvent) {
});
}
try {
const r = await event.locals.pb.collection('trails_bounding_box').getOne<TrailBoundingBox>(event.locals.pb.authStore.record!.id)
const r = await event.locals.pb.collection('trails_bounding_box').getOne<TrailBoundingBox>(event.locals.user.actor!)
return json(r)
} catch (e: any) {
throw handleError(e);
return handleError(e);
}
}

View File

@@ -16,6 +16,6 @@ export async function POST(event: RequestEvent) {
},
});
} catch (e) {
throw handleError(e)
return handleError(e)
}
}

View File

@@ -14,9 +14,9 @@ export async function GET(event: RequestEvent) {
});
}
try {
const r = await event.locals.pb.collection('trails_filter').getOne<TrailFilterValues>(event.locals.pb.authStore.record!.id)
const r = await event.locals.pb.collection('trails_filter').getOne<TrailFilterValues>(event.locals.user.actor)
return json(r)
} catch (e: any) {
throw handleError(e);
return handleError(e);
}
}

View File

@@ -0,0 +1,20 @@
import type { Trail } from '$lib/models/trail';
import { Collection, handleError, uploadCreate } from '$lib/util/api_util';
import { json, type RequestEvent } from '@sveltejs/kit';
export async function PUT(event: RequestEvent) {
try {
const r = await uploadCreate<Trail>(event, Collection.trails)
enrichRecord(r);
return json(r);
} catch (e) {
return handleError(e)
}
}
function enrichRecord(r: Trail) {
r.date = r.date?.substring(0, 10) ?? "";
for (const log of r.expand?.summit_logs_via_trail ?? []) {
log.date = log.date.substring(0, 10);
}
}

View File

@@ -0,0 +1,21 @@
import type { Trail } from "$lib/models/trail";
import { Collection, handleError, uploadUpdate } from "$lib/util/api_util";
import { json, type RequestEvent } from "@sveltejs/kit";
export async function POST(event: RequestEvent) {
try {
const r = await uploadUpdate<Trail>(event, Collection.trails)
enrichRecord(r);
return json(r);
} catch (e) {
return handleError(e)
}
}
function enrichRecord(r: Trail) {
r.date = r.date?.substring(0, 10) ?? "";
for (const log of r.expand?.summit_logs_via_trail ?? []) {
log.date = log.date.substring(0, 10);
}
}

View File

@@ -1,5 +1,4 @@
import { TrailRecommendSchema } from '$lib/models/api/trail_schema';
import type { Trail } from '$lib/models/trail';
import { handleError } from '$lib/util/api_util';
import { json, type RequestEvent } from '@sveltejs/kit';
@@ -7,27 +6,13 @@ export async function GET(event: RequestEvent) {
try {
const searchParams = Object.fromEntries(event.url.searchParams);
const safeSearchParams = TrailRecommendSchema.parse(searchParams);
const r = await event.locals.pb.send("/trail/recommend?" + new URLSearchParams({
size: safeSearchParams.size?.toString() ?? ""
}), {
method: "GET",
});
const result: Trail[] = r;
const numberOfTrails = (await event.locals.ms.index("trails").search("", {limit: 1})).estimatedTotalHits
const randomOffset = (safeSearchParams.size ?? 0) > numberOfTrails ? 0 : Math.floor(Math.random() * (numberOfTrails - 1) + 1)
const response = await event.locals.ms.index("trails").search("", {limit: safeSearchParams.size, offset: randomOffset})
for (const t of result) {
if (!t.author || !event.locals.pb.authStore.record) {
continue;
}
if (!t.expand) {
t.expand = {} as any
}
t.expand!.author = await event.locals.pb.collection("users_anonymous").getOne(t.author);
}
return json(result)
return json(response.hits)
} catch (e: any) {
throw handleError(e);
return handleError(e);
}
}

View File

@@ -1,12 +1,13 @@
import GPX from "$lib/models/gpx/gpx";
import { haversineDistance } from "$lib/models/gpx/utils";
import type { Trail } from "$lib/models/trail";
import type { Trail, TrailSearchResult } from "$lib/models/trail";
import { trails_create } from "$lib/stores/trail_store";
import { handleError } from "$lib/util/api_util";
import { fromFile, gpx2trail } from "$lib/util/gpx_util";
import { json, type RequestEvent } from "@sveltejs/kit";
import type MeiliSearch from "meilisearch";
import type { Hits } from "meilisearch";
import { ClientResponseError } from "pocketbase";
import type PocketBase from 'pocketbase';
export async function PUT(event: RequestEvent) {
try {
@@ -28,14 +29,14 @@ export async function PUT(event: RequestEvent) {
const ignoreDuplicates = data.get("ignoreDuplicates") === "true"
if (!ignoreDuplicates) {
let duplicate: Trail | null = null;
let duplicate: TrailSearchResult | null = null;
try {
duplicate = await findDuplicate(event.locals.pb, trail)
duplicate = await findDuplicate(event.locals.ms, trail)
} catch (e: any) {
throw new ClientResponseError({ status: 500, response: { message: "Error checking for duplicates" } })
}
if (duplicate !== null) {
throw new ClientResponseError({ status: 400, response: { message: `Duplicate trail`, id: duplicate.id, name: duplicate.name }, })
throw new ClientResponseError({ status: 400, response: { message: `Duplicate trail`, id: duplicate.id, name: duplicate.name, domain: `${duplicate.author_name}${duplicate.domain ? '@' + duplicate.domain : ''}` }, })
}
}
@@ -56,17 +57,19 @@ export async function PUT(event: RequestEvent) {
trail = await trails_create(trail, [], gpxFile, event.fetch, event.locals.user);
} catch (e: any) {
console.error(e)
throw handleError(e)
return handleError(e)
}
return json(trail);
} catch (e: any) {
throw handleError(e)
return handleError(e)
}
}
async function findDuplicate(pb: PocketBase, t1: Trail) {
const trails: Trail[] = await pb.collection('trails').getFullList({ requestKey: null });
async function findDuplicate(ms: MeiliSearch, t1: Trail) {
const response = await ms.index("trails").search("", {});
const trails: TrailSearchResult[] = response.hits as Hits<TrailSearchResult>
const distanceThreshold = 100;
const elevationThreshhold = 50;
@@ -76,7 +79,7 @@ async function findDuplicate(pb: PocketBase, t1: Trail) {
const lengthDifference = Math.abs((t1.distance ?? 0) - (t2.distance ?? 0));
const elevationGainDifference = Math.abs((t1.elevation_gain ?? 0) - (t2.elevation_gain ?? 0));
const elevationLossDifference = Math.abs((t1.elevation_loss ?? 0) - (t2.elevation_loss ?? 0));
const startpointDifference = haversineDistance(t1.lat ?? 0, t1.lon ?? 0, t2.lat ?? 0, t2.lon ?? 0)
const startpointDifference = haversineDistance(t1.lat ?? 0, t1.lon ?? 0, t2._geo.lat ?? 0, t2._geo.lng ?? 0)
if (lengthDifference < lengthThreshhold && elevationGainDifference < elevationThreshhold && elevationLossDifference < elevationThreshhold && startpointDifference < distanceThreshold) {
return t2

View File

@@ -21,7 +21,7 @@ export async function PUT(event: RequestEvent) {
return json(r);
} catch (e: any) {
throw handleError(e);
return handleError(e);
}
}

View File

@@ -10,7 +10,7 @@ export async function GET(event: RequestEvent) {
return json(r)
} catch (e: any) {
throw handleError(e);
return handleError(e);
}
}
@@ -34,7 +34,7 @@ export async function POST(event: RequestEvent) {
return json(r);
}
} catch (e: any) {
throw handleError(e);
return handleError(e);
}
}
@@ -43,6 +43,6 @@ export async function DELETE(event: RequestEvent) {
const r = await remove(event, Collection.users)
return json(r);
} catch (e: any) {
throw handleError(e);
return handleError(e);
}
}

View File

@@ -17,7 +17,7 @@ export async function GET(event: RequestEvent) {
return json(r)
} catch (e: any) {
throw handleError(e);
return handleError(e);
}
}

View File

@@ -8,6 +8,6 @@ export async function GET(event: RequestEvent) {
return json(r)
} catch (e: any) {
throw handleError(e);
return handleError(e);
}
}

View File

@@ -8,7 +8,7 @@ export async function GET(event: RequestEvent) {
const r = await list<Waypoint>(event, Collection.waypoints);
return json(r)
} catch (e: any) {
throw handleError(e);
return handleError(e);
}
}
@@ -17,6 +17,6 @@ export async function PUT(event: RequestEvent) {
const r = await create<Waypoint>(event, WaypointCreateSchema, Collection.waypoints)
return json(r);
} catch (e) {
throw handleError(e)
return handleError(e)
}
}

View File

@@ -8,7 +8,7 @@ export async function GET(event: RequestEvent) {
const r = await show<Waypoint>(event, Collection.waypoints)
return json(r)
} catch (e: any) {
throw handleError(e)
return handleError(e)
}
}
@@ -17,7 +17,7 @@ export async function POST(event: RequestEvent) {
const r = await update<Waypoint>(event, WaypointUpdateSchema, Collection.waypoints)
return json(r);
} catch (e: any) {
throw handleError(e)
return handleError(e)
}
}
@@ -26,6 +26,6 @@ export async function DELETE(event: RequestEvent) {
const r = await remove(event, Collection.waypoints)
return json(r);
} catch (e: any) {
throw handleError(e)
return handleError(e)
}
}

View File

@@ -7,6 +7,6 @@ export async function POST(event: RequestEvent) {
const r = await upload<Waypoint>(event, Collection.waypoints);
return json(r);
} catch (e: any) {
throw handleError(e);
return handleError(e);
}
}