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

@@ -0,0 +1,14 @@
import type { User } from "../user";
export interface Activity {
id: string;
iri: string;
type: string;
to: string[];
cc: string[];
bto: string[];
bcc: string[];
object: any;
actor: string;
published: string;
}

View File

@@ -0,0 +1,25 @@
import type { User } from "../user";
export interface Actor {
id?: string;
username: string;
preferred_username: string;
domain?: string;
summary?: string;
published?: string;
followerCount?: number,
followingCount?: number,
iri: string;
inbox: string;
outbox?: string;
icon?: string;
followers?: string;
following?: string;
isLocal: boolean;
public_key: string;
last_fetched: string;
user?: string
expand?: {
user?: User
}
}

View File

@@ -0,0 +1,12 @@
export interface WebfingerResponse {
subject: string
aliases: string[]
links: Link[]
}
export interface Link {
rel: string
type: string
href: string
}

View File

@@ -3,15 +3,13 @@ import type { Comment } from "../comment";
const CommentCreateSchema = z.object({
text: z.string(),
rating: z.number().optional(),
author: z.string().length(15),
trail: z.string().length(15),
trail: z.string(),
}) satisfies ZodType<Comment>
const CommentUpdateSchema = z.object({
text: z.string().optional(),
rating: z.number().optional(),
}) satisfies ZodType<Partial<Comment>>
export { CommentCreateSchema, CommentUpdateSchema }

View File

@@ -2,10 +2,9 @@ import { z, ZodType } from "zod";
import type { Follow } from "../follow";
const FollowCreateSchema = z.object({
follower: z.string().length(15),
followee: z.string().length(15),
}) satisfies ZodType<Follow>
})
export { FollowCreateSchema };

View File

@@ -2,7 +2,7 @@ import { z, ZodType } from "zod";
import type { ListShare } from "../list_share";
const ListShareCreateSchema = z.object({
user: z.string().length(15),
actor: z.string().url(),
list: z.string().length(15),
permission: z.enum(["view", "edit"])

View File

@@ -12,11 +12,12 @@ const SummitLogCreateSchema = z.object({
elevation_loss: z.number().nonnegative().optional(),
duration: z.number().nonnegative().optional(),
author: z.string().length(15),
trail: z.string().length(15).optional(),
photos: z.array(z.string()).default([])
}) satisfies ZodType<Partial<SummitLog>>
const SummitLogUpdateSchema = z.object({
date: z.string().date().refine((val) => !val || !isNaN(Date.parse(val)), "invalid-date").optional(),
date: z.string().date().refine((val) => !val || !isNaN(Date.parse(val)), "invalid-date").optional(),
text: z.string().optional(),
gpx: z.string().optional(),
distance: z.number().nonnegative().optional(),

View File

@@ -0,0 +1,10 @@
import { z, ZodType } from "zod";
import type { TrailLike } from "../trail_like";
const TrailLikeCreateSchema = z.object({
actor: z.string().length(15),
trail: z.string().length(15),
}) satisfies ZodType<TrailLike>
export { TrailLikeCreateSchema };

View File

@@ -19,7 +19,7 @@ const TrailCreateSchema = z.object({
photos: z.array(z.string()).default([]),
thumbnail: z.number().int().nonnegative().optional(),
waypoints: z.array(z.string()).default([]),
summit_logs: z.array(z.string()).default([]),
like_count: z.number().int().min(0).optional().default(0),
category: z.string().length(15).optional().or(z.literal('')),
tags: z.array(z.string()).default([]),
gpx: z.string().optional(),
@@ -45,7 +45,7 @@ const TrailUpdateSchema = z.object({
"photos+": z.string().optional(),
thumbnail: z.number().int().nonnegative().optional(),
waypoints: z.array(z.string()).optional(),
summit_logs: z.array(z.string()).optional(),
like_count: z.number().int().min(0).optional().default(0),
category: z.string().optional(),
tags: z.array(z.string()).optional(),
gpx: z.string().optional(),

View File

@@ -2,7 +2,7 @@ import { z, ZodType } from "zod";
import type { TrailShare } from "../trail_share";
const TrailShareCreateSchema = z.object({
user: z.string().length(15),
actor: z.string().url(),
trail: z.string().length(15),
permission: z.enum(["view", "edit"])

View File

@@ -1,20 +1,19 @@
import type { UserAnonymous } from "./user";
import type { Actor } from "./activitypub/actor";
export class Comment {
id?: string;
text: string;
rating?: number;
author: string;
trail: string;
created?: string;
updated?: string;
iri?: string;
expand?: {
author: UserAnonymous
author: Actor
}
constructor(text: string, rating: number, author: string, trail: string) {
constructor(text: string, author: string, trail: string) {
this.text = text;
this.rating = rating;
this.author = author;
this.trail = trail;
}

View File

@@ -1,16 +1,18 @@
import type { User, UserAnonymous } from "./user";
import type { Actor } from "./activitypub/actor";
export class Follow {
id?: string;
follower: string;
followee: string;
status: "pending" | "accepted";
expand?: {
follower: UserAnonymous,
followee: UserAnonymous
follower: Actor,
followee: Actor
}
constructor(follower: string, followee: string) {
this.follower = follower;
this.followee = followee;
this.status = "pending"
}
}

View File

@@ -1,18 +1,24 @@
import type { Trail } from "./trail";
import type { ListShare } from "./list_share";
import type { UserAnonymous } from "./user";
import type { Actor } from "./activitypub/actor";
export class List {
id?: string;
name: string;
public: boolean;
description?: string;
elevation_gain?: number;
elevation_loss?: number;
distance?: number;
duration?: number;
avatar?: string;
trails?: string[];
iri?: string;
expand?: {
trails?: Trail[]
list_share_via_list?: ListShare[]
author?: UserAnonymous;
author?: Actor;
}
author: string;
@@ -22,7 +28,7 @@ export class List {
this.public = params?.public ?? false
this.expand = { trails: trails };
this.trails = trails.map(t => t.id!);
this.description = params?.description;
this.description = params?.description ?? "";
this.avatar = params?.avatar;
this.author = params?.author ?? "000000000000000";
}
@@ -35,35 +41,4 @@ export interface ListFilter {
public?: boolean;
shared?: boolean;
sortOrder?: "+" | "-"
}
export const enum ExpandType {
None = 0,
Trails = 1 << 0,
Waypoints = 1 << 1,
TrailCategories = 1 << 2,
ListShares = 1 << 3,
All = ~(~0 << 4),
}
export function ExpandTypeToString(e: ExpandType) : string {
if (e == ExpandType.None)
return "";
var ret = "";
if ((e & ExpandType.Trails) === ExpandType.Trails) {
ret += "trails,";
}
if ((e & ExpandType.Waypoints) === ExpandType.Waypoints) {
ret += "trails.waypoints,";
}
if ((e & ExpandType.TrailCategories) === ExpandType.TrailCategories) {
ret += "trails.category,";
}
if ((e & ExpandType.ListShares) === ExpandType.ListShares) {
ret += "list_share_via_list,";
}
return ret.slice(0, -1);
}
}

View File

@@ -1,16 +1,17 @@
import type { Actor } from "./activitypub/actor";
import type { User } from "./user";
export class ListShare {
id?: string;
user: string;
actor: string;
list: string;
permission: "view" | "edit"
expand?: {
user: User
actor: Actor
}
constructor(user: string, list: string, permission: "view" | "edit") {
this.user = user;
this.actor = user;
this.list = list;
this.permission = permission
}

View File

@@ -1,12 +1,16 @@
import type { UserAnonymous } from "./user";
import type { Actor } from "./activitypub/actor";
enum NotificationType {
trailCreate = "trail_create",
trailShare = "trail_share",
listCreate = "list_create",
trailLike = "trail_like",
listShare = "list_share",
newFollower = "new_follower",
trailComment = "trail_comment"
trailComment = "trail_comment",
summitLogCreate = "summit_log_create",
commentMention = "comment_mention",
trailMention = "trail_mention",
summitLogMention = "summit_log_mention",
};
interface Notification {
@@ -18,9 +22,9 @@ interface Notification {
author: string
created: string;
expand: {
recipient: UserAnonymous;
author: UserAnonymous;
recipient: Actor;
author: Actor;
}
}
export { type Notification, NotificationType }
export { NotificationType, type Notification };

View File

@@ -0,0 +1,15 @@
import type { DateTime } from "activitypub-types";
export interface Profile {
id: string;
username: string;
preferredUsername: string;
acct: string;
bio: string;
followers: number;
following: number;
uri: string;
icon: string;
createdAt: DateTime;
error?: string;
}

View File

@@ -1,5 +1,5 @@
import type { Actor } from "./activitypub/actor";
import type { Trail } from "./trail";
import type { UserAnonymous } from "./user";
class SummitLog {
id?: string;
@@ -14,11 +14,14 @@ class SummitLog {
elevation_loss?: number
duration?: number
author: string;
trail?: string;
iri?: string;
created?: string;
expand?: {
gpx_data?: string;
trails_via_summit_logs?: Trail[];
author?: UserAnonymous
trail?: Trail;
author?: Actor
}
constructor(date: string, params?: { id?: string, text?: string, distance?: number, elevation_loss?: number, elevation_gain?: number, duration?: number, photos?: string[] }) {
@@ -41,6 +44,7 @@ interface SummitLogFilter {
category: string[],
startDate?: string;
endDate?: string;
trail?: string;
}
export { SummitLog, type SummitLogFilter };

View File

@@ -1,6 +1,9 @@
interface Activity {
interface TimelineItem {
id: string;
trail_id: string;
trail_author_username: string;
trail_author_domain: string;
trail_iri: string;
name: string,
description: string;
date: string;
@@ -15,4 +18,4 @@ interface Activity {
created: string;
}
export { type Activity }
export { type TimelineItem }

View File

@@ -1,9 +1,10 @@
import type { Actor } from "./activitypub/actor";
import type { Category } from "./category";
import type { Comment } from "./comment";
import type { SummitLog } from "./summit_log";
import type { Tag } from "./tag";
import type { TrailLike } from "./trail_like";
import type { TrailShare } from "./trail_share";
import type { UserAnonymous } from "./user";
import type { Waypoint } from "./waypoint";
class Trail {
@@ -26,17 +27,21 @@ class Trail {
category?: string;
tags: string[];
waypoints: string[];
summit_logs: string[];
polyline?: string;
domain?: string;
iri?: string;
like_count: number;
expand?: {
tags?: Tag[]
category?: Category;
waypoints?: Waypoint[]
summit_logs?: SummitLog[]
author?: UserAnonymous
summit_logs_via_trail?: SummitLog[]
author?: Actor
comments_via_trail?: Comment[]
gpx_data?: string
trail_share_via_trail?: TrailShare[]
trail_like_via_trail?: TrailLike[]
}
description?: string;
author: string;
@@ -83,13 +88,13 @@ class Trail {
this.thumbnail = params?.thumbnail ?? 0;
this.photos = params?.photos ?? [];
this.waypoints = [];
this.summit_logs = [];
this.tags = []
this.gpx = params?.gpx;
this.like_count = 0
this.expand = {
category: params?.category,
waypoints: params?.waypoints ?? [],
summit_logs: params?.summit_logs ?? [],
summit_logs_via_trail: params?.summit_logs ?? [],
comments_via_trail: params?.comments ?? [],
trail_share_via_trail: params?.shares ?? []
}
@@ -125,6 +130,7 @@ interface TrailFilter {
startDate?: string;
endDate?: string;
completed?: boolean;
liked?: boolean;
sort: "name" | "distance" | "elevation_gain" | "created";
sortOrder: "+" | "-"
}
@@ -168,8 +174,12 @@ interface TrailSearchResult {
public: boolean;
thumbnail: string;
polyline?: string;
likes?: string[];
like_count: number;
shares?: string[];
tags?: string[]
domain?: string;
iri?: string;
gpx: string;
_geo: {
lat: number,
@@ -196,12 +206,16 @@ export const defaultTrailSearchAttributes = [
"created",
"public",
"thumbnail",
"domain",
"gpx",
"tags",
"like_count",
"shares",
"iri",
"_geo",]
export { Trail };
export type { TrailBoundingBox, TrailFilter, TrailFilterValues, TrailSearchResult };

View File

@@ -0,0 +1,17 @@
import type { Actor } from "./activitypub/actor";
import type { Trail } from "./trail";
export class TrailLike {
id?: string;
actor: string;
trail: string;
expand?: {
actor: Actor,
trail?: Trail
}
constructor(actor: string, trail: string) {
this.actor = actor;
this.trail = trail;
}
}

View File

@@ -1,16 +1,16 @@
import type { User, UserAnonymous } from "./user";
import type { Actor } from "./activitypub/actor";
export class TrailShare {
id?: string;
user: string;
actor: string;
trail: string;
permission: "view" | "edit"
expand?: {
user: UserAnonymous
actor: Actor
}
constructor(user: string, trail: string, permission: "view" | "edit") {
this.user = user;
constructor(actor: string, trail: string, permission: "view" | "edit") {
this.actor = actor;
this.trail = trail;
this.permission = permission
}

View File

@@ -7,7 +7,6 @@ export type User = AuthRecord & {
email?: string,
password: string,
avatar?: string;
bio?: string;
created?: string;
}
@@ -15,6 +14,7 @@ export type UserAnonymous = {
id: string,
username?: string,
avatar?: string;
bio?: string;
created?: string;
private: boolean;
}