425 Commits

Author SHA1 Message Date
github-actions[bot]
51bed0d3f3 chore: release v0.19.1 (#995)
Co-authored-by: Flomp <26000991+Flomp@users.noreply.github.com>
2026-05-14 18:47:06 +02:00
slothful-vassal
bfa8dfefbd prepare v0.19.1 (#988)
* prepare v0.19.1

* update changelog

---------

Co-authored-by: Christian Beutel <>
Co-authored-by: Flomp <Flomp@users.noreply.github.com>
2026-05-14 18:41:14 +02:00
dependabot[bot]
7b7b841659 Bump astro from 6.1.9 to 6.3.2 (with dependencies) in /docs (#992)
* Bump astro from 6.1.9 to 6.3.1 in /docs

Bumps [astro](https://github.com/withastro/astro/tree/HEAD/packages/astro) from 6.1.9 to 6.3.1.
- [Release notes](https://github.com/withastro/astro/releases)
- [Changelog](https://github.com/withastro/astro/blob/main/packages/astro/CHANGELOG.md)
- [Commits](https://github.com/withastro/astro/commits/astro@6.3.1/packages/astro)

---
updated-dependencies:
- dependency-name: astro
  dependency-version: 6.3.1
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>

* Update Astro adapter dependencies

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: slothful-vassal <89943360+slothful-vassal@users.noreply.github.com>
2026-05-13 20:31:34 +02:00
slothful-vassal
f69ad01029 optimize db view queries (#729)
* optimize db view queries

* add more indices

* appease gofmt

---------

Co-authored-by: Flomp <Flomp@users.noreply.github.com>
Co-authored-by: Christian Beutel <>
2026-05-13 19:11:01 +02:00
slothful-vassal
611a7b5f02 Fix/apiDocsBuild (#989)
* copy api json to docs

* copy api json to docs

* improvements

* fix web build

* mjs > js
2026-05-12 11:28:04 +02:00
Flomp
0bb55ef84e Fix comment author rule (#984)
Co-authored-by: Christian Beutel <>
2026-05-11 20:03:18 +02:00
Flomp
c9024a4322 Fix trails being unavailable when profile is private (#986) 2026-05-11 20:02:37 +02:00
Flomp
ea6f5d5c95 initial commit (#987)
Co-authored-by: Christian Beutel <>
2026-05-11 19:48:43 +02:00
Flomp
b2753c29d2 initial commit (#980)
Co-authored-by: Christian Beutel <>
2026-05-11 11:11:09 +02:00
slothful-vassal
868010459f fix docker build, prevent wrong directory after build (#979) 2026-05-10 19:36:36 +02:00
github-actions[bot]
be8de33c4d chore: release v0.19.0 (#975)
Co-authored-by: Flomp <26000991+Flomp@users.noreply.github.com>
2026-05-10 18:41:55 +02:00
slothful-vassal
677d3a12b5 prepare changelog for upcoming release (#970)
* prepare changelog for upcoming release

* minor wording improvements

* #960

* #961

* update changelog

---------

Co-authored-by: Flomp <Flomp@users.noreply.github.com>
Co-authored-by: Christian Beutel <>
2026-05-10 10:11:48 +02:00
slothful-vassal
1b2781960a fix: resolve Svelte 5 reactivity warnings (#974)
- Use untrack() for $derived/$state initial values captured outside reactive context (scene.svelte, lists edit page)
- Add $state() to bind:this component references in settings account page

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Flomp <Flomp@users.noreply.github.com>
2026-05-10 08:42:20 +02:00
slothful-vassal
edad99c7b4 fix email change issues (#973)
* fix email change

* isolated email update

* update email UI immediately

---------

Co-authored-by: Christian Beutel <>
2026-05-10 08:38:55 +02:00
Flomp
d2ac49470a Federation Refactoring & Architecture Improvements (#930)
* initial commit

* add lists

* update permissions

* fix waypoint create

* needs_full_sync for activitypub trails

* fixes build issues

* fix list get

* further CSRF protection

* update API docs

* adds rate limiter

* fix tiptap mentions

* a bit more cleanup of main.go

* Fix migration order

* fixes reviewed notes

* require context for activitpub server calls

* improve hashing for identifier

* fix copy paste error

* fix sync trail/list issues

* fix summit log/comment duplicates

* adaptions after trail merge

* fix dockerignore

---------

Co-authored-by: Christian Beutel <>
Co-authored-by: slothful-vassal <89943360+slothful-vassal@users.noreply.github.com>
2026-05-09 17:30:34 +02:00
slothful-vassal
992ebfc0c4 prevent uploading gpx twice when creating a trail (#969) 2026-05-09 14:07:32 +02:00
slothful-vassal
b5739cb72e merge trails function added (#627)
* merge trails function added

* fix conflict resolve issues

* avoid incorrect authorship when merging trail comments

* require edit access for all selected trails before merge

* fix likes checkbox binding in trail merge modal

* improve error handling

* require editable target and deletable sources for merge

* avoid duplicate trail fetch during merge

* prevent duplicate likes when merging trails

* check photo download responses during trail merge

* add merge completion toast notifications

* cleanup

* small fixes

* move merge code to backend, option to merge single trail selection with similar one, automatic merge for integrations add as option, maintenance page to find and merge similar trails added

* fix build error

* fix

* docu, beautifying, refactoring

---------

Co-authored-by: Flomp <Flomp@users.noreply.github.com>
2026-05-09 13:09:26 +02:00
slothful-vassal
d2268429c7 chore/pocketbase v0.38.0 (#972)
Co-authored-by: Flomp <Flomp@users.noreply.github.com>
2026-05-09 13:02:31 +02:00
dependabot[bot]
f1e2ae4088 Bump postcss from 8.5.8 to 8.5.14 in /docs (#965)
fixes a security issue
2026-05-09 12:25:29 +02:00
dependabot[bot]
17343bac9e Bump fast-uri from 3.1.0 to 3.1.2 in /docs (#971)
fixes two security issues
2026-05-09 12:20:42 +02:00
dependabot[bot]
817616e801 Bump the docker-backward-compatible group in /docs with 7 updates (#953)
Bumps the docker-backward-compatible group in /docs with 7 updates:

| Package | From | To |
| --- | --- | --- |
| [@astrojs/node](https://github.com/withastro/astro/tree/HEAD/packages/integrations/node) | `10.0.5` | `10.0.6` |
| [@astrojs/starlight](https://github.com/withastro/starlight/tree/HEAD/packages/starlight) | `0.38.3` | `0.38.4` |
| [@tailwindcss/vite](https://github.com/tailwindlabs/tailwindcss/tree/HEAD/packages/@tailwindcss-vite) | `4.2.2` | `4.2.4` |
| [astro](https://github.com/withastro/astro/tree/HEAD/packages/astro) | `6.1.7` | `6.1.9` |
| [starlight-openapi](https://github.com/HiDeoo/starlight-openapi/tree/HEAD/packages/starlight-openapi) | `0.24.0` | `0.25.0` |
| [svelte](https://github.com/sveltejs/svelte/tree/HEAD/packages/svelte) | `5.55.4` | `5.55.5` |
| [tailwindcss](https://github.com/tailwindlabs/tailwindcss/tree/HEAD/packages/tailwindcss) | `4.2.2` | `4.2.4` |


Updates `@astrojs/node` from 10.0.5 to 10.0.6
- [Release notes](https://github.com/withastro/astro/releases)
- [Changelog](https://github.com/withastro/astro/blob/main/packages/integrations/node/CHANGELOG.md)
- [Commits](https://github.com/withastro/astro/commits/@astrojs/node@10.0.6/packages/integrations/node)

Updates `@astrojs/starlight` from 0.38.3 to 0.38.4
- [Release notes](https://github.com/withastro/starlight/releases)
- [Changelog](https://github.com/withastro/starlight/blob/main/packages/starlight/CHANGELOG.md)
- [Commits](https://github.com/withastro/starlight/commits/@astrojs/starlight@0.38.4/packages/starlight)

Updates `@tailwindcss/vite` from 4.2.2 to 4.2.4
- [Release notes](https://github.com/tailwindlabs/tailwindcss/releases)
- [Changelog](https://github.com/tailwindlabs/tailwindcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/tailwindlabs/tailwindcss/commits/v4.2.4/packages/@tailwindcss-vite)

Updates `astro` from 6.1.7 to 6.1.9
- [Release notes](https://github.com/withastro/astro/releases)
- [Changelog](https://github.com/withastro/astro/blob/main/packages/astro/CHANGELOG.md)
- [Commits](https://github.com/withastro/astro/commits/astro@6.1.9/packages/astro)

Updates `starlight-openapi` from 0.24.0 to 0.25.0
- [Release notes](https://github.com/HiDeoo/starlight-openapi/releases)
- [Changelog](https://github.com/HiDeoo/starlight-openapi/blob/main/packages/starlight-openapi/CHANGELOG.md)
- [Commits](https://github.com/HiDeoo/starlight-openapi/commits/starlight-openapi@0.25.0/packages/starlight-openapi)

Updates `svelte` from 5.55.4 to 5.55.5
- [Release notes](https://github.com/sveltejs/svelte/releases)
- [Changelog](https://github.com/sveltejs/svelte/blob/main/packages/svelte/CHANGELOG.md)
- [Commits](https://github.com/sveltejs/svelte/commits/svelte@5.55.5/packages/svelte)

Updates `tailwindcss` from 4.2.2 to 4.2.4
- [Release notes](https://github.com/tailwindlabs/tailwindcss/releases)
- [Changelog](https://github.com/tailwindlabs/tailwindcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/tailwindlabs/tailwindcss/commits/v4.2.4/packages/tailwindcss)

---
updated-dependencies:
- dependency-name: "@astrojs/node"
  dependency-version: 10.0.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: docker-backward-compatible
- dependency-name: "@astrojs/starlight"
  dependency-version: 0.38.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: docker-backward-compatible
- dependency-name: "@tailwindcss/vite"
  dependency-version: 4.2.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: docker-backward-compatible
- dependency-name: astro
  dependency-version: 6.1.9
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: docker-backward-compatible
- dependency-name: starlight-openapi
  dependency-version: 0.25.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: docker-backward-compatible
- dependency-name: svelte
  dependency-version: 5.55.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: docker-backward-compatible
- dependency-name: tailwindcss
  dependency-version: 4.2.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: docker-backward-compatible
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: slothful-vassal <89943360+slothful-vassal@users.noreply.github.com>
2026-05-09 12:10:18 +02:00
Pål Håland
54e2fc2fe6 fix: error handling fallback in search endpoints (#961) 2026-05-08 22:19:43 +02:00
Pål Håland
a11a4cb3cb fix: layer manager bug and null safety fixes (#960) 2026-05-08 22:13:39 +02:00
Pål Håland
127b7a0745 chore: meilisearch sdk rename and dependency upgrades (#958)
Co-authored-by: slothful-vassal <89943360+slothful-vassal@users.noreply.github.com>
2026-05-07 22:58:06 +02:00
Pål Håland
25fe7982c5 chore: dockerfile build optimizations (#959) 2026-05-07 21:33:46 +02:00
slothful-vassal
5cc6f7bd55 fix broken webfinger requests (#966) 2026-05-07 10:42:49 +02:00
Matteo Fortini
798970daa3 Fix syntax error in query selector for color scheme meta tag (#957) 2026-05-05 20:50:38 +02:00
slothful-vassal
6d1d1236ef Trail Edit / Waypoints from photos: prevent creating different waypoints for near-by locations (#457)
* trail edit / waypoints from photos: prevent creating different waypoints for near-by locations

* remove not necessary imports

* remove changes from gitignore

* patch gitignore

* waypoint merge radius as category property

* change datamodel for merge radius (use more generic settings field for future implementations)

* fix npm run check issues

* several improvements/fixes

* fix

* move geo cluster code to db

* fix compile issue

* fix docker issue

* merge with existing waypoints

---------

Co-authored-by: Christian Beutel <>
Co-authored-by: Flomp <Flomp@users.noreply.github.com>
2026-04-29 11:15:06 +02:00
slothful-vassal
85e5fb6df7 server sided external api calls (#697)
* server sided external api calls

* fix uploading gpx files

* rename api/v1/nominatim with api/v1/geocoding to prepare photon support

* remove NOMINATIM_MAX_RETRIES (side-kick from other PR)

* re-add nominatim url fallback

* docu, env var defaults and fallbacks, docker compose files

* refactor

* furhter refactorings and fixes

* PUBLIC_VALHALLA_ENABLED in docker compose files added

* fix env var names

---------

Co-authored-by: Flomp <Flomp@users.noreply.github.com>
2026-04-28 18:19:06 +02:00
Flomp
7bdf0d3151 Adds option to mark trail as completed (#920)
* initial commit

* add completed for komoot tours

* mark hammerhead activities as completed

* fix review suggestions

* adds completed modal to trail edit page

---------

Co-authored-by: Christian Beutel <>
Co-authored-by: slothful-vassal <89943360+slothful-vassal@users.noreply.github.com>
2026-04-27 20:37:11 +02:00
slothful-vassal
7e84e39cda fix hillshading visibility (#942)
Co-authored-by: Flomp <Flomp@users.noreply.github.com>
2026-04-27 16:31:58 +02:00
slothful-vassal
86c94a5ce2 fix komoot integration issue with 'invalid' photos (#941) 2026-04-27 16:17:00 +02:00
dependabot[bot]
35a5d657bf Bump @astrojs/starlight-tailwind from 4.0.2 to 5.0.0 in /docs (#905)
Bumps [@astrojs/starlight-tailwind](https://github.com/withastro/starlight/tree/HEAD/packages/tailwind) from 4.0.2 to 5.0.0.
- [Release notes](https://github.com/withastro/starlight/releases)
- [Changelog](https://github.com/withastro/starlight/blob/main/packages/tailwind/CHANGELOG.md)
- [Commits](https://github.com/withastro/starlight/commits/@astrojs/starlight-tailwind@5.0.0/packages/tailwind)

---
updated-dependencies:
- dependency-name: "@astrojs/starlight-tailwind"
  dependency-version: 5.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-23 19:58:43 +02:00
dependabot[bot]
41ee45d0e4 Bump the docker-backward-compatible group across 1 directory with 4 updates (#945)
Bumps the docker-backward-compatible group with 4 updates in the /docs directory: [@astrojs/starlight](https://github.com/withastro/starlight/tree/HEAD/packages/starlight), [@astrojs/svelte](https://github.com/withastro/astro/tree/HEAD/packages/integrations/svelte), [astro](https://github.com/withastro/astro/tree/HEAD/packages/astro) and [svelte](https://github.com/sveltejs/svelte/tree/HEAD/packages/svelte).


Updates `@astrojs/starlight` from 0.38.2 to 0.38.3
- [Release notes](https://github.com/withastro/starlight/releases)
- [Changelog](https://github.com/withastro/starlight/blob/main/packages/starlight/CHANGELOG.md)
- [Commits](https://github.com/withastro/starlight/commits/@astrojs/starlight@0.38.3/packages/starlight)

Updates `@astrojs/svelte` from 8.0.3 to 8.0.5
- [Release notes](https://github.com/withastro/astro/releases)
- [Changelog](https://github.com/withastro/astro/blob/main/packages/integrations/svelte/CHANGELOG.md)
- [Commits](https://github.com/withastro/astro/commits/@astrojs/svelte@8.0.5/packages/integrations/svelte)

Updates `astro` from 6.0.8 to 6.1.7
- [Release notes](https://github.com/withastro/astro/releases)
- [Changelog](https://github.com/withastro/astro/blob/main/packages/astro/CHANGELOG.md)
- [Commits](https://github.com/withastro/astro/commits/astro@6.1.7/packages/astro)

Updates `svelte` from 5.55.0 to 5.55.4
- [Release notes](https://github.com/sveltejs/svelte/releases)
- [Changelog](https://github.com/sveltejs/svelte/blob/main/packages/svelte/CHANGELOG.md)
- [Commits](https://github.com/sveltejs/svelte/commits/svelte@5.55.4/packages/svelte)

---
updated-dependencies:
- dependency-name: "@astrojs/starlight"
  dependency-version: 0.38.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: docker-backward-compatible
- dependency-name: "@astrojs/svelte"
  dependency-version: 8.0.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: docker-backward-compatible
- dependency-name: astro
  dependency-version: 6.1.7
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: docker-backward-compatible
- dependency-name: svelte
  dependency-version: 5.55.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: docker-backward-compatible
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-23 19:44:43 +02:00
dependabot[bot]
75a7e08433 Bump @astrojs/node from 10.0.3 to 10.0.5 in /docs (#944)
Bumps [@astrojs/node](https://github.com/withastro/astro/tree/HEAD/packages/integrations/node) from 10.0.3 to 10.0.5.
- [Release notes](https://github.com/withastro/astro/releases)
- [Changelog](https://github.com/withastro/astro/blob/main/packages/integrations/node/CHANGELOG.md)
- [Commits](https://github.com/withastro/astro/commits/@astrojs/node@10.0.5/packages/integrations/node)

---
updated-dependencies:
- dependency-name: "@astrojs/node"
  dependency-version: 10.0.5
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-23 19:39:44 +02:00
Flomp
345452e720 Improve frontpage performance (#929)
* initial commit

* merge with origin/main

* fix package-lock.json

* fix tiptap mentions

* remove flutter

* update package-lock.json

* disable tailwindcss/typography until https://github.com/tailwindlabs/tailwindcss/issues/19946 is fixed

* reactivate tailwind/typography after fix

---------

Co-authored-by: Christian Beutel <>
Co-authored-by: slothful-vassal <89943360+slothful-vassal@users.noreply.github.com>
2026-04-22 09:04:53 +02:00
slothful-vassal
c9256aeb29 fix help links (#938) 2026-04-22 09:03:48 +02:00
Flomp
5457057bb2 Add metadata to exported waypoints in GPX (#919)
* initial commit

* consider wpt.sym as icon

* fix icons typing

---------

Co-authored-by: Christian Beutel <>
2026-04-20 21:04:32 +02:00
Flomp
25c63ffa56 Improves multiselect (#921)
* initial commit

* fixes open in new tab

---------

Co-authored-by: Christian Beutel <>
2026-04-20 21:04:00 +02:00
Stefan Schlögl
19fd259129 fix komoot sync stopping before all tours are imported (#917)
Three bugs caused partial imports:

1. Type filter halted pagination: if a page contained only tours of the
   excluded type (planned vs. recorded), syncTrailWithTours returned false
   and the loop stopped even though later pages might have matching tours.
   Fixed by tracking allAlreadySynced separately - only tours already in the
   DB count toward early termination, not type-filtered ones.

2. continue on error never incremented page: a fetch or sync error called
   continue on the outer for loop, so page stayed the same and hasNewTours
   was never updated, causing an infinite retry on the failing page. Changed
   to break so a failed page aborts the current sync cycle cleanly.

3. Pagination ignored TotalPages: the loop used a fragile stop heuristic
   instead of the TotalPages value returned by the Komoot API. fetchTours
   now returns TotalPages and the loop uses it as the upper bound, with the
   allAlreadySynced early-exit kept as an optimization for incremental syncs.

Closes #570

Co-authored-by: slothful-vassal <89943360+slothful-vassal@users.noreply.github.com>
2026-04-20 20:24:51 +02:00
Flomp
871bf38112 Feature: watcher based bulk uploads (#886)
* initial commit

* fix watcher.js for docker

* Update docs

* update healthcheck

* update docs

---------

Co-authored-by: Christian Beutel <>
2026-04-20 13:46:32 +02:00
Pål Håland
1f17a8784b Improved and updated Norwegian translation (#931)
Som places with plural translation had not been updated correctly, these
are now handled. In addition the hammerhead integration adden some new
translations, and these are now translated.

Co-authored-by: Flomp <Flomp@users.noreply.github.com>
2026-04-19 11:13:53 +02:00
Flomp
b7b16e5a6e Adds API docs annotations & generator (#927)
* initial commit

* fixes category api route

* fixes follow api routes

---------

Co-authored-by: Christian Beutel <>
2026-04-18 17:04:49 +02:00
dependabot[bot]
da38cfc19a Bump golang.org/x/net (#932)
Bumps the gomod-backward-compatible group with 1 update in the /db directory: [golang.org/x/net](https://github.com/golang/net).


Updates `golang.org/x/net` from 0.50.0 to 0.53.0
- [Commits](https://github.com/golang/net/compare/v0.50.0...v0.53.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-version: 0.53.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: gomod-backward-compatible
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-18 11:59:32 +02:00
dependabot[bot]
814009c463 Bump docker/setup-buildx-action from 3 to 4 (#906)
Bumps [docker/setup-buildx-action](https://github.com/docker/setup-buildx-action) from 3 to 4.
- [Release notes](https://github.com/docker/setup-buildx-action/releases)
- [Commits](https://github.com/docker/setup-buildx-action/compare/v3...v4)

---
updated-dependencies:
- dependency-name: docker/setup-buildx-action
  dependency-version: '4'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-18 11:58:39 +02:00
dependabot[bot]
9b3afbface Bump docker/login-action from 3 to 4 (#908)
Bumps [docker/login-action](https://github.com/docker/login-action) from 3 to 4.
- [Release notes](https://github.com/docker/login-action/releases)
- [Commits](https://github.com/docker/login-action/compare/v3...v4)

---
updated-dependencies:
- dependency-name: docker/login-action
  dependency-version: '4'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-18 11:57:56 +02:00
dependabot[bot]
bef362914f Bump golang.org/x/image from 0.36.0 to 0.38.0 in /db (#901)
Bumps [golang.org/x/image](https://github.com/golang/image) from 0.36.0 to 0.38.0.
- [Commits](https://github.com/golang/image/compare/v0.36.0...v0.38.0)

---
updated-dependencies:
- dependency-name: golang.org/x/image
  dependency-version: 0.38.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-18 11:56:34 +02:00
dependabot[bot]
5279ab4279 Bump vite, @sveltejs/vite-plugin-svelte and vitest in /web (#915)
* Bump vite, @sveltejs/vite-plugin-svelte and vitest in /web

Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite), [@sveltejs/vite-plugin-svelte](https://github.com/sveltejs/vite-plugin-svelte/tree/HEAD/packages/vite-plugin-svelte) and [vitest](https://github.com/vitest-dev/vitest/tree/HEAD/packages/vitest). These dependencies needed to be updated together.

Updates `vite` from 5.4.21 to 8.0.8
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v8.0.8/packages/vite)

Updates `@sveltejs/vite-plugin-svelte` from 4.0.4 to 7.0.0
- [Release notes](https://github.com/sveltejs/vite-plugin-svelte/releases)
- [Changelog](https://github.com/sveltejs/vite-plugin-svelte/blob/main/packages/vite-plugin-svelte/CHANGELOG.md)
- [Commits](https://github.com/sveltejs/vite-plugin-svelte/commits/@sveltejs/vite-plugin-svelte@7.0.0/packages/vite-plugin-svelte)

Updates `vitest` from 1.6.1 to 4.1.4
- [Release notes](https://github.com/vitest-dev/vitest/releases)
- [Commits](https://github.com/vitest-dev/vitest/commits/v4.1.4/packages/vitest)

---
updated-dependencies:
- dependency-name: vite
  dependency-version: 8.0.8
  dependency-type: direct:development
- dependency-name: "@sveltejs/vite-plugin-svelte"
  dependency-version: 7.0.0
  dependency-type: direct:development
- dependency-name: vitest
  dependency-version: 4.1.4
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>

* fix conflict

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Flomp <Flomp@users.noreply.github.com>
Co-authored-by: Christian Beutel <>
2026-04-15 14:03:06 +02:00
dependabot[bot]
012a416862 Bump @xmldom/xmldom from 0.8.11 to 0.8.12 in /web (#902)
Bumps [@xmldom/xmldom](https://github.com/xmldom/xmldom) from 0.8.11 to 0.8.12.
- [Release notes](https://github.com/xmldom/xmldom/releases)
- [Changelog](https://github.com/xmldom/xmldom/blob/master/CHANGELOG.md)
- [Commits](https://github.com/xmldom/xmldom/compare/0.8.11...0.8.12)

---
updated-dependencies:
- dependency-name: "@xmldom/xmldom"
  dependency-version: 0.8.12
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Flomp <Flomp@users.noreply.github.com>
2026-04-15 13:53:48 +02:00
dependabot[bot]
1ca1431207 Bump picomatch from 4.0.2 to 4.0.4 in /web (#897)
Bumps [picomatch](https://github.com/micromatch/picomatch) from 4.0.2 to 4.0.4.
- [Release notes](https://github.com/micromatch/picomatch/releases)
- [Changelog](https://github.com/micromatch/picomatch/blob/master/CHANGELOG.md)
- [Commits](https://github.com/micromatch/picomatch/compare/4.0.2...4.0.4)

---
updated-dependencies:
- dependency-name: picomatch
  dependency-version: 4.0.4
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Flomp <Flomp@users.noreply.github.com>
2026-04-15 13:48:01 +02:00
dependabot[bot]
b2e188f845 Bump marked from 16.2.0 to 17.0.3 in /web (#858)
Bumps [marked](https://github.com/markedjs/marked) from 16.2.0 to 17.0.3.
- [Release notes](https://github.com/markedjs/marked/releases)
- [Commits](https://github.com/markedjs/marked/compare/v16.2.0...v17.0.3)

---
updated-dependencies:
- dependency-name: marked
  dependency-version: 17.0.3
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Flomp <Flomp@users.noreply.github.com>
2026-04-15 13:32:53 +02:00
dependabot[bot]
7375f03818 Bump vite from 7.3.1 to 7.3.2 in /docs (#913)
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 7.3.1 to 7.3.2.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/v7.3.2/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v7.3.2/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-version: 7.3.2
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Flomp <Flomp@users.noreply.github.com>
2026-04-15 13:22:31 +02:00
dependabot[bot]
f9bf5372d5 Bump @sveltejs/kit from 2.53.4 to 2.57.1 in /web (#916)
Bumps [@sveltejs/kit](https://github.com/sveltejs/kit/tree/HEAD/packages/kit) from 2.53.4 to 2.57.1.
- [Release notes](https://github.com/sveltejs/kit/releases)
- [Changelog](https://github.com/sveltejs/kit/blob/main/packages/kit/CHANGELOG.md)
- [Commits](https://github.com/sveltejs/kit/commits/@sveltejs/kit@2.57.1/packages/kit)

---
updated-dependencies:
- dependency-name: "@sveltejs/kit"
  dependency-version: 2.57.1
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-15 13:19:24 +02:00
slothful-vassal
f6354dcd56 narrow-wide trail list view mode added (#666)
* narrow-wide trail list view mode added

* change icon, change pos, add i18n

* change icon

---------

Co-authored-by: Christian Beutel <>
Co-authored-by: Flomp <Flomp@users.noreply.github.com>
2026-04-01 19:07:27 +02:00
Flomp
591a72e2d1 initial commit (#896)
Co-authored-by: Christian Beutel <>
2026-03-26 20:54:40 +01:00
dependabot[bot]
812bf5f626 Bump devalue from 5.6.3 to 5.6.4 in /docs (#887)
* Bump devalue from 5.6.3 to 5.6.4 in /docs

Bumps [devalue](https://github.com/sveltejs/devalue) from 5.6.3 to 5.6.4.
- [Release notes](https://github.com/sveltejs/devalue/releases)
- [Changelog](https://github.com/sveltejs/devalue/blob/main/CHANGELOG.md)
- [Commits](https://github.com/sveltejs/devalue/compare/v5.6.3...v5.6.4)

---
updated-dependencies:
- dependency-name: devalue
  dependency-version: 5.6.4
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

* fix dependencies

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Flomp <Flomp@users.noreply.github.com>
Co-authored-by: Christian Beutel <>
2026-03-25 14:28:11 +01:00
dependabot[bot]
2430bfe671 Bump @astrojs/node from 9.5.4 to 10.0.0 in /docs (#895)
Bumps [@astrojs/node](https://github.com/withastro/astro/tree/HEAD/packages/integrations/node) from 9.5.4 to 10.0.0.
- [Release notes](https://github.com/withastro/astro/releases)
- [Changelog](https://github.com/withastro/astro/blob/main/packages/integrations/node/CHANGELOG.md)
- [Commits](https://github.com/withastro/astro/commits/@astrojs/node@10.0.0/packages/integrations/node)

---
updated-dependencies:
- dependency-name: "@astrojs/node"
  dependency-version: 10.0.0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Flomp <Flomp@users.noreply.github.com>
2026-03-25 13:59:07 +01:00
dependabot[bot]
fa5234a390 Bump h3 from 1.15.5 to 1.15.9 in /docs (#894)
Bumps [h3](https://github.com/h3js/h3) from 1.15.5 to 1.15.9.
- [Release notes](https://github.com/h3js/h3/releases)
- [Changelog](https://github.com/h3js/h3/blob/v1.15.9/CHANGELOG.md)
- [Commits](https://github.com/h3js/h3/compare/v1.15.5...v1.15.9)

---
updated-dependencies:
- dependency-name: h3
  dependency-version: 1.15.9
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-25 13:56:33 +01:00
Flomp
ec76b69dba Fix mentions when username != preferred_username (#885)
* initial commit

* fixes mention name

---------

Co-authored-by: Christian Beutel <>
2026-03-20 19:35:34 +01:00
Flomp
04818b9329 Switch to better FIT parser (#884)
* initial commit

* Fix undefined Buffer for clientside upload

---------

Co-authored-by: Christian Beutel <>
2026-03-20 19:04:50 +01:00
slothful-vassal
369e71fcec hammerhead integration added (#628)
* hammerhead integration added

* typo corrected

* start date added, optimize some translations

* new option to send trail to hammerhead

* fix elevation information (was in cm)

* skip empty tracks on import

* update docs

* remove manual user-id input

* fix merge issues

* remove user-id from documentation

* fix hammerhead logo for light theme

* fix hammerhead logo for light theme

---------

Co-authored-by: Christian Beutel <>
Co-authored-by: Flomp <Flomp@users.noreply.github.com>
2026-03-20 13:58:34 +01:00
dependabot[bot]
5a05573e83 Bump jspdf from 4.2.0 to 4.2.1 in /web (#890)
Bumps [jspdf](https://github.com/parallax/jsPDF) from 4.2.0 to 4.2.1.
- [Release notes](https://github.com/parallax/jsPDF/releases)
- [Changelog](https://github.com/parallax/jsPDF/blob/master/RELEASE.md)
- [Commits](https://github.com/parallax/jsPDF/compare/v4.2.0...v4.2.1)

---
updated-dependencies:
- dependency-name: jspdf
  dependency-version: 4.2.1
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-20 13:17:59 +01:00
Flomp
83a64775d0 initial commit (#883)
Co-authored-by: Christian Beutel <>
2026-03-10 21:49:10 +01:00
Flomp
0623287206 initial commit (#881)
Co-authored-by: Christian Beutel <>
2026-03-10 21:48:54 +01:00
Flomp
debb1bee31 initial commit (#882)
Co-authored-by: Christian Beutel <>
2026-03-10 21:48:39 +01:00
slothful-vassal
4b1dc784c9 fix issues updating avatar (#870) 2026-03-10 18:00:38 +01:00
slothful-vassal
c9acf8ad91 fix list descriptions in list selection modal (#869) 2026-03-07 10:23:30 +01:00
Flomp
711a592038 Feature: API Tokens (#848)
* initial commit

* fix last_used timestamp

---------

Co-authored-by: Christian Beutel <>
2026-03-07 10:10:25 +01:00
Flomp
ba88d08078 Update meilisearch to v1.36.0 (#847)
* Update meilisearch to v1.36.0

* Merge remote-tracking branch 'origin/main' into chore/update-meilisearch

---------

Co-authored-by: Christian Beutel <>
2026-03-07 10:00:31 +01:00
dependabot[bot]
039650b950 Bump peter-evans/create-pull-request from 6 to 8 (#853)
Bumps [peter-evans/create-pull-request](https://github.com/peter-evans/create-pull-request) from 6 to 8.
- [Release notes](https://github.com/peter-evans/create-pull-request/releases)
- [Commits](https://github.com/peter-evans/create-pull-request/compare/v6...v8)

---
updated-dependencies:
- dependency-name: peter-evans/create-pull-request
  dependency-version: '8'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-07 08:43:32 +01:00
dependabot[bot]
71a5ddadc8 Bump dompurify from 3.3.1 to 3.3.2 in /web (#871)
Bumps [dompurify](https://github.com/cure53/DOMPurify) from 3.3.1 to 3.3.2.
- [Release notes](https://github.com/cure53/DOMPurify/releases)
- [Commits](https://github.com/cure53/DOMPurify/compare/3.3.1...3.3.2)

---
updated-dependencies:
- dependency-name: dompurify
  dependency-version: 3.3.2
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-07 08:42:41 +01:00
dependabot[bot]
143fa8e73e Bump @sveltejs/kit from 2.52.2 to 2.53.4 in /web (#845)
Bumps [@sveltejs/kit](https://github.com/sveltejs/kit/tree/HEAD/packages/kit) from 2.52.2 to 2.53.4.
- [Release notes](https://github.com/sveltejs/kit/releases)
- [Changelog](https://github.com/sveltejs/kit/blob/main/packages/kit/CHANGELOG.md)
- [Commits](https://github.com/sveltejs/kit/commits/@sveltejs/kit@2.53.4/packages/kit)

---
updated-dependencies:
- dependency-name: "@sveltejs/kit"
  dependency-version: 2.53.4
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-07 00:05:48 +01:00
dependabot[bot]
0c24173f3f Bump @types/node from 20.17.14 to 25.3.3 in /web (#857)
Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 20.17.14 to 25.3.3.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-version: 25.3.3
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-06 23:58:49 +01:00
dependabot[bot]
6cd7a20bad Bump svgo from 4.0.0 to 4.0.1 in /docs (#864)
Bumps [svgo](https://github.com/svg/svgo) from 4.0.0 to 4.0.1.
- [Release notes](https://github.com/svg/svgo/releases)
- [Commits](https://github.com/svg/svgo/compare/v4.0.0...v4.0.1)

---
updated-dependencies:
- dependency-name: svgo
  dependency-version: 4.0.1
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-06 23:58:18 +01:00
Jonas Plum
84bf27a415 Fix go version in Dockerfile (#859)
* Add dockerfile updates and cooldown settings to dependabot

* Upgrade Go version in Dockerfile to 1.25.0
2026-03-01 19:35:13 +01:00
Flomp
dd77f21d6a Initial commit (#862)
Co-authored-by: Christian Beutel <>
2026-03-01 19:34:26 +01:00
Flomp
88aabfa6fd Update pocketbase to 36.5 (#846)
Co-authored-by: Christian Beutel <>
2026-03-01 19:32:11 +01:00
slothful-vassal
f2396ae574 new trail view options: create copy & change visibility (#571)
* change visibility of multiple trails in trails view (multi-select)

* error handling

* create copy option added

* wait for meilisearch when updating trail

* remove toggles from dropdown

* change public/private logic

* adds loading spinner for bulk operations

* fixes trail duplicate

* fixes tags delete on visiblility change

* fix conflict resolving issue

* edit labels & german translation

---------

Co-authored-by: Christian Beutel <>
Co-authored-by: Flomp <Flomp@users.noreply.github.com>
2026-03-01 18:18:09 +01:00
dependabot[bot]
2b9d5305a8 Bump the gomod-backward-compatible group in /db with 2 updates (#851)
Bumps the gomod-backward-compatible group in /db with 2 updates: [github.com/pocketbase/dbx](https://github.com/pocketbase/dbx) and [golang.org/x/net](https://github.com/golang/net).


Updates `github.com/pocketbase/dbx` from 1.11.0 to 1.12.0
- [Release notes](https://github.com/pocketbase/dbx/releases)
- [Commits](https://github.com/pocketbase/dbx/compare/v1.11.0...v1.12.0)

Updates `golang.org/x/net` from 0.49.0 to 0.51.0
- [Commits](https://github.com/golang/net/compare/v0.49.0...v0.51.0)

---
updated-dependencies:
- dependency-name: github.com/pocketbase/dbx
  dependency-version: 1.12.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: gomod-backward-compatible
- dependency-name: golang.org/x/net
  dependency-version: 0.51.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: gomod-backward-compatible
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-01 07:22:10 +01:00
dependabot[bot]
3d862c64e8 Bump actions/checkout from 4 to 6 (#849)
Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to 6.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v4...v6)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-01 07:10:29 +01:00
dependabot[bot]
bebfb08e1c Bump the docker-backward-compatible group in /docs with 3 updates (#854)
Bumps the docker-backward-compatible group in /docs with 3 updates: [@tailwindcss/vite](https://github.com/tailwindlabs/tailwindcss/tree/HEAD/packages/@tailwindcss-vite), [astro](https://github.com/withastro/astro/tree/HEAD/packages/astro) and [tailwindcss](https://github.com/tailwindlabs/tailwindcss/tree/HEAD/packages/tailwindcss).


Updates `@tailwindcss/vite` from 4.1.18 to 4.2.1
- [Release notes](https://github.com/tailwindlabs/tailwindcss/releases)
- [Changelog](https://github.com/tailwindlabs/tailwindcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/tailwindlabs/tailwindcss/commits/v4.2.1/packages/@tailwindcss-vite)

Updates `astro` from 5.17.3 to 5.18.0
- [Release notes](https://github.com/withastro/astro/releases)
- [Changelog](https://github.com/withastro/astro/blob/astro@5.18.0/packages/astro/CHANGELOG.md)
- [Commits](https://github.com/withastro/astro/commits/astro@5.18.0/packages/astro)

Updates `tailwindcss` from 4.1.18 to 4.2.1
- [Release notes](https://github.com/tailwindlabs/tailwindcss/releases)
- [Changelog](https://github.com/tailwindlabs/tailwindcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/tailwindlabs/tailwindcss/commits/v4.2.1/packages/tailwindcss)

---
updated-dependencies:
- dependency-name: "@tailwindcss/vite"
  dependency-version: 4.2.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: docker-backward-compatible
- dependency-name: astro
  dependency-version: 5.18.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: docker-backward-compatible
- dependency-name: tailwindcss
  dependency-version: 4.2.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: docker-backward-compatible
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-01 07:07:23 +01:00
dependabot[bot]
a00070a9eb Bump actions/setup-go from 5 to 6 (#850)
Bumps [actions/setup-go](https://github.com/actions/setup-go) from 5 to 6.
- [Release notes](https://github.com/actions/setup-go/releases)
- [Commits](https://github.com/actions/setup-go/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/setup-go
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-01 07:05:26 +01:00
dependabot[bot]
cde1f73288 Bump actions/setup-node from 4 to 6 (#852)
Bumps [actions/setup-node](https://github.com/actions/setup-node) from 4 to 6.
- [Release notes](https://github.com/actions/setup-node/releases)
- [Commits](https://github.com/actions/setup-node/compare/v4...v6)

---
updated-dependencies:
- dependency-name: actions/setup-node
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-01 07:05:04 +01:00
slothful-vassal
bd293e6739 new setting: begin drawing a new trail from current location (#592)
* new setting: begin drawing a new trail from  current location

* apply only when drawing

* fix application when editing an existing route

* documentation

* ignore pointer events on location circles

* fix i18n

---------

Co-authored-by: Flomp <Flomp@users.noreply.github.com>
Co-authored-by: Christian Beutel <>
2026-02-28 10:15:43 +01:00
slothful-vassal
230f42d639 Improve save public list (#554)
* improve save public list

* remove obsolete code fragment

---------

Co-authored-by: Flomp <Flomp@users.noreply.github.com>
2026-02-28 09:26:48 +01:00
dependabot[bot]
536276e0b3 Bump rollup from 4.31.0 to 4.59.0 in /web (#840)
Bumps [rollup](https://github.com/rollup/rollup) from 4.31.0 to 4.59.0.
- [Release notes](https://github.com/rollup/rollup/releases)
- [Changelog](https://github.com/rollup/rollup/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rollup/rollup/compare/v4.31.0...v4.59.0)

---
updated-dependencies:
- dependency-name: rollup
  dependency-version: 4.59.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-28 07:06:27 +01:00
dependabot[bot]
a0eaedd156 Bump @astrojs/node from 9.5.2 to 9.5.4 in /docs (#836)
Bumps [@astrojs/node](https://github.com/withastro/astro/tree/HEAD/packages/integrations/node) from 9.5.2 to 9.5.4.
- [Release notes](https://github.com/withastro/astro/releases)
- [Changelog](https://github.com/withastro/astro/blob/@astrojs/node@9.5.4/packages/integrations/node/CHANGELOG.md)
- [Commits](https://github.com/withastro/astro/commits/@astrojs/node@9.5.4/packages/integrations/node)

---
updated-dependencies:
- dependency-name: "@astrojs/node"
  dependency-version: 9.5.4
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-28 07:05:36 +01:00
dependabot[bot]
31171e15f6 Bump rollup from 4.43.0 to 4.59.0 in /docs (#842)
Bumps [rollup](https://github.com/rollup/rollup) from 4.43.0 to 4.59.0.
- [Release notes](https://github.com/rollup/rollup/releases)
- [Changelog](https://github.com/rollup/rollup/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rollup/rollup/compare/v4.43.0...v4.59.0)

---
updated-dependencies:
- dependency-name: rollup
  dependency-version: 4.59.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-28 07:04:56 +01:00
dependabot[bot]
6c9b41342a Bump svelte from 5.53.0 to 5.53.6 in /web (#843)
Bumps [svelte](https://github.com/sveltejs/svelte/tree/HEAD/packages/svelte) from 5.53.0 to 5.53.6.
- [Release notes](https://github.com/sveltejs/svelte/releases)
- [Changelog](https://github.com/sveltejs/svelte/blob/main/packages/svelte/CHANGELOG.md)
- [Commits](https://github.com/sveltejs/svelte/commits/svelte@5.53.6/packages/svelte)

---
updated-dependencies:
- dependency-name: svelte
  dependency-version: 5.53.6
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-28 07:04:18 +01:00
dependabot[bot]
21fe188fdc Bump svelte from 5.53.0 to 5.53.6 in /docs (#844)
Bumps [svelte](https://github.com/sveltejs/svelte/tree/HEAD/packages/svelte) from 5.53.0 to 5.53.6.
- [Release notes](https://github.com/sveltejs/svelte/releases)
- [Changelog](https://github.com/sveltejs/svelte/blob/main/packages/svelte/CHANGELOG.md)
- [Commits](https://github.com/sveltejs/svelte/commits/svelte@5.53.6/packages/svelte)

---
updated-dependencies:
- dependency-name: svelte
  dependency-version: 5.53.6
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-28 07:03:46 +01:00
Flomp
6cac905368 Use dynamic tokens & bootstrap meili config on startup (#839)
* initial commit

* persist meili token in cookie

* handle login/logout cookie invalidation

* fix review suggestions

---------

Co-authored-by: Christian Beutel <>
2026-02-27 08:52:53 +01:00
dependabot[bot]
bb8b4fbc2e Bump devalue from 5.6.2 to 5.6.3 in /docs (#828)
Bumps [devalue](https://github.com/sveltejs/devalue) from 5.6.2 to 5.6.3.
- [Release notes](https://github.com/sveltejs/devalue/releases)
- [Changelog](https://github.com/sveltejs/devalue/blob/main/CHANGELOG.md)
- [Commits](https://github.com/sveltejs/devalue/compare/v5.6.2...v5.6.3)

---
updated-dependencies:
- dependency-name: devalue
  dependency-version: 5.6.3
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Flomp <Flomp@users.noreply.github.com>
Co-authored-by: Jonas Plum <cugu@users.noreply.github.com>
2026-02-20 20:01:26 +01:00
dependabot[bot]
573eb9df1f Bump svelte from 5.49.1 to 5.53.0 in /docs (#829)
Bumps [svelte](https://github.com/sveltejs/svelte/tree/HEAD/packages/svelte) from 5.49.1 to 5.53.0.
- [Release notes](https://github.com/sveltejs/svelte/releases)
- [Changelog](https://github.com/sveltejs/svelte/blob/main/packages/svelte/CHANGELOG.md)
- [Commits](https://github.com/sveltejs/svelte/commits/svelte@5.53.0/packages/svelte)

---
updated-dependencies:
- dependency-name: svelte
  dependency-version: 5.53.0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: slothful-vassal <89943360+slothful-vassal@users.noreply.github.com>
2026-02-20 19:53:00 +01:00
slothful-vassal
34039f3d43 fix/readme: version linked to wrong docker image (#833) 2026-02-20 19:52:40 +01:00
github-actions[bot]
702681acee Release v0.18.5 (#830)
* chore: release v0.18.5

* update changelog

---------

Co-authored-by: Flomp <26000991+Flomp@users.noreply.github.com>
Co-authored-by: Christian Beutel <>
2026-02-20 14:50:42 +01:00
Flomp
23b0913c9a initial commit (#826)
Co-authored-by: Christian Beutel <>
2026-02-20 13:56:58 +01:00
Flomp
623bc7b036 Fixes race condition in crosshair plugin (#827) 2026-02-20 13:39:55 +01:00
dependabot[bot]
70ffac6902 Bump svelte from 5.49.2 to 5.53.0 in /web (#825)
Bumps [svelte](https://github.com/sveltejs/svelte/tree/HEAD/packages/svelte) from 5.49.2 to 5.53.0.
- [Release notes](https://github.com/sveltejs/svelte/releases)
- [Changelog](https://github.com/sveltejs/svelte/blob/main/packages/svelte/CHANGELOG.md)
- [Commits](https://github.com/sveltejs/svelte/commits/svelte@5.53.0/packages/svelte)

---
updated-dependencies:
- dependency-name: svelte
  dependency-version: 5.53.0
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Flomp <Flomp@users.noreply.github.com>
2026-02-20 12:30:41 +01:00
dependabot[bot]
9973540fce Bump @sveltejs/kit from 2.50.2 to 2.52.2 in /web (#824)
Bumps [@sveltejs/kit](https://github.com/sveltejs/kit/tree/HEAD/packages/kit) from 2.50.2 to 2.52.2.
- [Release notes](https://github.com/sveltejs/kit/releases)
- [Changelog](https://github.com/sveltejs/kit/blob/main/packages/kit/CHANGELOG.md)
- [Commits](https://github.com/sveltejs/kit/commits/@sveltejs/kit@2.52.2/packages/kit)

---
updated-dependencies:
- dependency-name: "@sveltejs/kit"
  dependency-version: 2.52.2
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-20 12:30:16 +01:00
dependabot[bot]
f6091ff01b Bump jspdf from 4.1.0 to 4.2.0 in /web (#823)
Bumps [jspdf](https://github.com/parallax/jsPDF) from 4.1.0 to 4.2.0.
- [Release notes](https://github.com/parallax/jsPDF/releases)
- [Changelog](https://github.com/parallax/jsPDF/blob/master/RELEASE.md)
- [Commits](https://github.com/parallax/jsPDF/compare/v4.1.0...v4.2.0)

---
updated-dependencies:
- dependency-name: jspdf
  dependency-version: 4.2.0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-20 12:28:34 +01:00
Martin Schmidt
be8465be78 fix: expand and display tag name in list view (#742)
Co-authored-by: Christian Beutel <>
Co-authored-by: Flomp <Flomp@users.noreply.github.com>
2026-02-20 11:00:04 +01:00
Lucas Werkmeister
b09b7d51a2 Upgrade @sveltejs/adapter-node to v5 (#791)
As far as I can tell, nothing breaks, and this includes support for
systemd socket activation which is quite nice.

Co-authored-by: Christian Beutel <>
2026-02-20 10:37:48 +01:00
slothful-vassal
088e09dc53 prevent losing trail filter settings (#814)
* prevent losing trail filter settings

* prevent page from jumping to top on filter update

---------

Co-authored-by: Christian Beutel <>
2026-02-20 10:27:07 +01:00
slothful-vassal
ff10084ba0 skip elevation correction if no valid height information returned by valhalla (#821) 2026-02-19 16:43:04 +01:00
slothful-vassal
c8af897cdd resolve svelte-check warnings (#819)
Co-authored-by: Jonas Plum <cugu@users.noreply.github.com>
2026-02-18 22:08:21 +01:00
slothful-vassal
5595f9b4d3 fix CVE-2022-39299 (bump xmldom) (#820) 2026-02-18 21:53:41 +01:00
dependabot[bot]
01d85b32f3 Bump @sveltejs/adapter-auto from 3.3.1 to 6.1.0 in /web (#491)
Bumps [@sveltejs/adapter-auto](https://github.com/sveltejs/kit/tree/HEAD/packages/adapter-auto) from 3.3.1 to 6.1.0.
- [Release notes](https://github.com/sveltejs/kit/releases)
- [Changelog](https://github.com/sveltejs/kit/blob/main/packages/adapter-auto/CHANGELOG.md)
- [Commits](https://github.com/sveltejs/kit/commits/@sveltejs/adapter-auto@6.1.0/packages/adapter-auto)

---
updated-dependencies:
- dependency-name: "@sveltejs/adapter-auto"
  dependency-version: 6.1.0
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-17 20:35:41 +01:00
dependabot[bot]
17a0f5cce8 Bump ajv from 8.17.1 to 8.18.0 in /docs (#818)
Bumps [ajv](https://github.com/ajv-validator/ajv) from 8.17.1 to 8.18.0.
- [Release notes](https://github.com/ajv-validator/ajv/releases)
- [Commits](https://github.com/ajv-validator/ajv/compare/v8.17.1...v8.18.0)

---
updated-dependencies:
- dependency-name: ajv
  dependency-version: 8.18.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-17 20:10:21 +01:00
dependabot[bot]
ae317afdbe Bump the npm-backward-compatible group across 1 directory with 22 updates (#787)
* Bump the npm-backward-compatible group across 1 directory with 22 updates

Bumps the npm-backward-compatible group with 21 updates in the /web directory:

| Package | From | To |
| --- | --- | --- |
| [@tailwindcss/typography](https://github.com/tailwindlabs/tailwindcss-typography) | `0.5.16` | `0.5.19` |
| [@tailwindcss/vite](https://github.com/tailwindlabs/tailwindcss/tree/HEAD/packages/@tailwindcss-vite) | `4.1.8` | `4.1.18` |
| [@threlte/core](https://github.com/threlte/threlte/tree/HEAD/packages/core) | `8.0.0-next.41` | `8.3.1` |
| [@threlte/extras](https://github.com/threlte/threlte/tree/HEAD/packages/extras) | `9.0.0-next.55` | `9.7.1` |
| [@turf/destination](https://github.com/Turfjs/turf) | `7.2.0` | `7.3.3` |
| [@turf/distance](https://github.com/Turfjs/turf) | `7.2.0` | `7.3.3` |
| [autoprefixer](https://github.com/postcss/autoprefixer) | `10.4.20` | `10.4.24` |
| [canvas-confetti](https://github.com/catdad/canvas-confetti) | `1.9.3` | `1.9.4` |
| [chart.js](https://github.com/chartjs/Chart.js) | `4.4.7` | `4.5.1` |
| [@types/chart.js](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/chart.js) | `2.9.41` | `4.0.1` |
| [json-diff-ts](https://github.com/ltwlf/json-diff-ts) | `4.8.1` | `4.8.2` |
| [meilisearch](https://github.com/meilisearch/meilisearch-js) | `0.37.0` | `0.55.0` |
| [pdfkit](https://github.com/foliojs/pdfkit) | `0.15.2` | `0.17.2` |
| [pocketbase](https://github.com/pocketbase/js-sdk) | `0.25.2` | `0.26.8` |
| [three](https://github.com/mrdoob/three.js) | `0.161.0` | `0.182.0` |
| [@types/three](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/three) | `0.161.2` | `0.182.0` |
| [@playwright/test](https://github.com/microsoft/playwright) | `1.58.1` | `1.58.2` |
| [postcss](https://github.com/postcss/postcss) | `8.5.1` | `8.5.6` |
| [svelte](https://github.com/sveltejs/svelte/tree/HEAD/packages/svelte) | `5.19.2` | `5.49.2` |
| [svelte-check](https://github.com/sveltejs/language-tools) | `4.1.4` | `4.3.6` |
| [typescript](https://github.com/microsoft/TypeScript) | `5.7.3` | `5.9.3` |



Updates `@tailwindcss/typography` from 0.5.16 to 0.5.19
- [Release notes](https://github.com/tailwindlabs/tailwindcss-typography/releases)
- [Changelog](https://github.com/tailwindlabs/tailwindcss-typography/blob/main/CHANGELOG.md)
- [Commits](https://github.com/tailwindlabs/tailwindcss-typography/compare/v0.5.16...v0.5.19)

Updates `@tailwindcss/vite` from 4.1.8 to 4.1.18
- [Release notes](https://github.com/tailwindlabs/tailwindcss/releases)
- [Changelog](https://github.com/tailwindlabs/tailwindcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/tailwindlabs/tailwindcss/commits/v4.1.18/packages/@tailwindcss-vite)

Updates `@threlte/core` from 8.0.0-next.41 to 8.3.1
- [Release notes](https://github.com/threlte/threlte/releases)
- [Changelog](https://github.com/threlte/threlte/blob/main/packages/core/CHANGELOG.md)
- [Commits](https://github.com/threlte/threlte/commits/@threlte/core@8.3.1/packages/core)

Updates `@threlte/extras` from 9.0.0-next.55 to 9.7.1
- [Release notes](https://github.com/threlte/threlte/releases)
- [Changelog](https://github.com/threlte/threlte/blob/main/packages/extras/CHANGELOG.md)
- [Commits](https://github.com/threlte/threlte/commits/@threlte/extras@9.7.1/packages/extras)

Updates `@turf/destination` from 7.2.0 to 7.3.3
- [Release notes](https://github.com/Turfjs/turf/releases)
- [Changelog](https://github.com/Turfjs/turf/blob/master/CHANGELOG.md)
- [Commits](https://github.com/Turfjs/turf/compare/v7.2.0...v7.3.3)

Updates `@turf/distance` from 7.2.0 to 7.3.3
- [Release notes](https://github.com/Turfjs/turf/releases)
- [Changelog](https://github.com/Turfjs/turf/blob/master/CHANGELOG.md)
- [Commits](https://github.com/Turfjs/turf/compare/v7.2.0...v7.3.3)

Updates `autoprefixer` from 10.4.20 to 10.4.24
- [Release notes](https://github.com/postcss/autoprefixer/releases)
- [Changelog](https://github.com/postcss/autoprefixer/blob/main/CHANGELOG.md)
- [Commits](https://github.com/postcss/autoprefixer/compare/10.4.20...10.4.24)

Updates `canvas-confetti` from 1.9.3 to 1.9.4
- [Release notes](https://github.com/catdad/canvas-confetti/releases)
- [Commits](https://github.com/catdad/canvas-confetti/compare/1.9.3...1.9.4)

Updates `chart.js` from 4.4.7 to 4.5.1
- [Release notes](https://github.com/chartjs/Chart.js/releases)
- [Commits](https://github.com/chartjs/Chart.js/compare/v4.4.7...v4.5.1)

Updates `@types/chart.js` from 2.9.41 to 4.0.1
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/chart.js)

Updates `json-diff-ts` from 4.8.1 to 4.8.2
- [Commits](https://github.com/ltwlf/json-diff-ts/compare/v4.8.1...v4.8.2)

Updates `meilisearch` from 0.37.0 to 0.55.0
- [Release notes](https://github.com/meilisearch/meilisearch-js/releases)
- [Commits](https://github.com/meilisearch/meilisearch-js/compare/v0.37.0...v0.55.0)

Updates `pdfkit` from 0.15.2 to 0.17.2
- [Release notes](https://github.com/foliojs/pdfkit/releases)
- [Changelog](https://github.com/foliojs/pdfkit/blob/master/CHANGELOG.md)
- [Commits](https://github.com/foliojs/pdfkit/compare/v0.15.2...v0.17.2)

Updates `pocketbase` from 0.25.2 to 0.26.8
- [Release notes](https://github.com/pocketbase/js-sdk/releases)
- [Changelog](https://github.com/pocketbase/js-sdk/blob/master/CHANGELOG.md)
- [Commits](https://github.com/pocketbase/js-sdk/compare/v0.25.2...v0.26.8)

Updates `three` from 0.161.0 to 0.182.0
- [Release notes](https://github.com/mrdoob/three.js/releases)
- [Commits](https://github.com/mrdoob/three.js/commits)

Updates `@types/three` from 0.161.2 to 0.182.0
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/three)

Updates `@playwright/test` from 1.58.1 to 1.58.2
- [Release notes](https://github.com/microsoft/playwright/releases)
- [Commits](https://github.com/microsoft/playwright/compare/v1.58.1...v1.58.2)

Updates `postcss` from 8.5.1 to 8.5.6
- [Release notes](https://github.com/postcss/postcss/releases)
- [Changelog](https://github.com/postcss/postcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/postcss/postcss/compare/8.5.1...8.5.6)

Updates `svelte` from 5.19.2 to 5.49.2
- [Release notes](https://github.com/sveltejs/svelte/releases)
- [Changelog](https://github.com/sveltejs/svelte/blob/main/packages/svelte/CHANGELOG.md)
- [Commits](https://github.com/sveltejs/svelte/commits/svelte@5.49.2/packages/svelte)

Updates `svelte-check` from 4.1.4 to 4.3.6
- [Release notes](https://github.com/sveltejs/language-tools/releases)
- [Commits](https://github.com/sveltejs/language-tools/compare/svelte-check-4.1.4...svelte-check@4.3.6)

Updates `tailwindcss` from 4.1.8 to 4.1.18
- [Release notes](https://github.com/tailwindlabs/tailwindcss/releases)
- [Changelog](https://github.com/tailwindlabs/tailwindcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/tailwindlabs/tailwindcss/commits/v4.1.18/packages/tailwindcss)

Updates `typescript` from 5.7.3 to 5.9.3
- [Release notes](https://github.com/microsoft/TypeScript/releases)
- [Commits](https://github.com/microsoft/TypeScript/compare/v5.7.3...v5.9.3)

---
updated-dependencies:
- dependency-name: "@tailwindcss/typography"
  dependency-version: 0.5.19
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: npm-backward-compatible
- dependency-name: "@tailwindcss/vite"
  dependency-version: 4.1.18
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: npm-backward-compatible
- dependency-name: "@threlte/core"
  dependency-version: 8.3.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: npm-backward-compatible
- dependency-name: "@threlte/extras"
  dependency-version: 9.7.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: npm-backward-compatible
- dependency-name: "@turf/destination"
  dependency-version: 7.3.3
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: npm-backward-compatible
- dependency-name: "@turf/distance"
  dependency-version: 7.3.3
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: npm-backward-compatible
- dependency-name: autoprefixer
  dependency-version: 10.4.24
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: npm-backward-compatible
- dependency-name: canvas-confetti
  dependency-version: 1.9.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: npm-backward-compatible
- dependency-name: chart.js
  dependency-version: 4.5.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: npm-backward-compatible
- dependency-name: "@types/chart.js"
  dependency-version: 4.0.1
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: npm-backward-compatible
- dependency-name: json-diff-ts
  dependency-version: 4.8.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: npm-backward-compatible
- dependency-name: meilisearch
  dependency-version: 0.55.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: npm-backward-compatible
- dependency-name: pdfkit
  dependency-version: 0.17.2
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: npm-backward-compatible
- dependency-name: pocketbase
  dependency-version: 0.26.8
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: npm-backward-compatible
- dependency-name: three
  dependency-version: 0.182.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: npm-backward-compatible
- dependency-name: "@types/three"
  dependency-version: 0.182.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: npm-backward-compatible
- dependency-name: "@playwright/test"
  dependency-version: 1.58.2
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: npm-backward-compatible
- dependency-name: postcss
  dependency-version: 8.5.6
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: npm-backward-compatible
- dependency-name: svelte
  dependency-version: 5.49.2
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: npm-backward-compatible
- dependency-name: svelte-check
  dependency-version: 4.3.6
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: npm-backward-compatible
- dependency-name: tailwindcss
  dependency-version: 4.1.18
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: npm-backward-compatible
- dependency-name: typescript
  dependency-version: 5.9.3
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: npm-backward-compatible
...

Signed-off-by: dependabot[bot] <support@github.com>

* Fix errors

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Jonas Plum <git@cugu.eu>
Co-authored-by: Jonas Plum <cugu@users.noreply.github.com>
2026-02-17 20:07:33 +01:00
slothful-vassal
0368892014 fix locale web error logs (#656)
* fix locale web error logs

* refactor(i18n): derive supported locales dynamically from locale files
2026-02-17 18:30:10 +01:00
slothful-vassal
0c715ad5ba list search modal: fix asynch states (#816) 2026-02-17 10:44:05 +01:00
slothful-vassal
adc32339b6 search field: fix losing focus, fix duplicated cancel button in chrome (#738) 2026-02-16 19:42:18 +01:00
slothful-vassal
6789a666ef fix gpx threshold handling (#813) 2026-02-16 19:41:37 +01:00
dependabot[bot]
67b80f01db Bump markdown-it from 14.1.0 to 14.1.1 in /web (#809) 2026-02-13 22:44:49 +01:00
Flomp
bdd83b10c7 adds new servers & removes stale ones (#804)
Co-authored-by: Christian Beutel <>
2026-02-12 21:23:56 +01:00
Jonas Plum
3a541d4d9a Only build amd64 image for dev release (#802)
Removed platform specification from Docker build commands.
2026-02-12 21:23:28 +01:00
slothful-vassal
840032e610 fix trail upload for users with default privacy settings (#785)
* fix trail upload for users with default privacy settings

* Fix optional chaining for trail privacy setting
2026-02-12 15:27:59 +01:00
Martin Schmidt
c0be0629e3 docs: auth providers and file size limitations (#743)
* feat: document file size and improve auth providers

* Minor text modifications

---------

Co-authored-by: Flomp <Flomp@users.noreply.github.com>
Co-authored-by: Christian Beutel <>
2026-02-12 14:00:15 +01:00
Robert Clarke
47672c5a9d fix vertical description spacing (#797) 2026-02-12 13:14:13 +01:00
Ole Albers
721c84c1d8 New Site added to servers (#799) 2026-02-12 13:10:48 +01:00
Ole Albers
98389d774d Fix installation link in README (#798)
* Fix installation link in README

Corrected the link for installation from source in README.

* Fix link to 'from source' installation

Updated link for installation from source documentation.

---------

Co-authored-by: slothful-vassal <89943360+slothful-vassal@users.noreply.github.com>
2026-02-11 10:49:39 +01:00
Jonas Plum
5a0118c175 Fix curl command syntax in cron.sh (#788) 2026-02-09 09:44:27 +01:00
Jonas Plum
687f474900 Set dev version for dev container (#790) 2026-02-07 14:14:47 +01:00
Jonas Plum
12713568e4 Publish dev docker images from main (#789)
* Publish unstable docker images from main

* Update and rename release_unstable.yaml to release_dev.yaml

* Add dev docker compose
2026-02-07 12:09:15 +01:00
dependabot[bot]
677f2bc473 Bump mdast-util-to-hast from 13.2.0 to 13.2.1 in /docs (#782)
Bumps [mdast-util-to-hast](https://github.com/syntax-tree/mdast-util-to-hast) from 13.2.0 to 13.2.1.
- [Release notes](https://github.com/syntax-tree/mdast-util-to-hast/releases)
- [Commits](https://github.com/syntax-tree/mdast-util-to-hast/compare/13.2.0...13.2.1)

---
updated-dependencies:
- dependency-name: mdast-util-to-hast
  dependency-version: 13.2.1
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-06 19:44:37 +01:00
dependabot[bot]
d74f43ddc6 Bump vitest from 1.6.0 to 1.6.1 in /web (#783)
Bumps [vitest](https://github.com/vitest-dev/vitest/tree/HEAD/packages/vitest) from 1.6.0 to 1.6.1.
- [Release notes](https://github.com/vitest-dev/vitest/releases)
- [Commits](https://github.com/vitest-dev/vitest/commits/v1.6.1/packages/vitest)

---
updated-dependencies:
- dependency-name: vitest
  dependency-version: 1.6.1
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-06 19:44:14 +01:00
dependabot[bot]
46a12647ee Bump @sveltejs/kit from 2.16.1 to 2.50.2 in /web (#772)
* Bump @sveltejs/kit from 2.16.1 to 2.50.2 in /web

Bumps [@sveltejs/kit](https://github.com/sveltejs/kit/tree/HEAD/packages/kit) from 2.16.1 to 2.50.2.
- [Release notes](https://github.com/sveltejs/kit/releases)
- [Changelog](https://github.com/sveltejs/kit/blob/main/packages/kit/CHANGELOG.md)
- [Commits](https://github.com/sveltejs/kit/commits/@sveltejs/kit@2.50.2/packages/kit)

---
updated-dependencies:
- dependency-name: "@sveltejs/kit"
  dependency-version: 2.50.2
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>

* Fix errors

* Fix errors

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Jonas Plum <git@cugu.eu>
Co-authored-by: Jonas Plum <cugu@users.noreply.github.com>
2026-02-06 13:31:37 +01:00
dependabot[bot]
f877e0b266 Bump dompurify and jspdf in /web (#777)
* Bump dompurify and jspdf in /web

Bumps [dompurify](https://github.com/cure53/DOMPurify) to 3.3.1 and updates ancestor dependency [jspdf](https://github.com/parallax/jsPDF). These dependencies need to be updated together.


Updates `dompurify` from 2.5.8 to 3.3.1
- [Release notes](https://github.com/cure53/DOMPurify/releases)
- [Commits](https://github.com/cure53/DOMPurify/compare/2.5.8...3.3.1)

Updates `jspdf` from 2.5.2 to 4.1.0
- [Release notes](https://github.com/parallax/jsPDF/releases)
- [Changelog](https://github.com/parallax/jsPDF/blob/master/RELEASE.md)
- [Commits](https://github.com/parallax/jsPDF/compare/v2.5.2...v4.1.0)

---
updated-dependencies:
- dependency-name: dompurify
  dependency-version: 3.3.1
  dependency-type: indirect
- dependency-name: jspdf
  dependency-version: 4.1.0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>

* fixes waypoint marker icon positions

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Christian Beutel <>
2026-02-06 12:40:42 +01:00
Jonas Plum
d1c87577de Fix healthcheck in docs (#781) 2026-02-06 11:49:54 +01:00
Jonas Plum
82ad995cdc Make the env var change non-breaking (#778)
* Fix typo in environment variable for sender address

Make the env var change non-breaking.

* Fix formatting of environment variable access
2026-02-06 11:49:19 +01:00
dependabot[bot]
301377527f Bump @babel/runtime from 7.26.0 to 7.28.6 in /web (#775)
Bumps [@babel/runtime](https://github.com/babel/babel/tree/HEAD/packages/babel-runtime) from 7.26.0 to 7.28.6.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.28.6/packages/babel-runtime)

---
updated-dependencies:
- dependency-name: "@babel/runtime"
  dependency-version: 7.28.6
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-05 09:37:46 +01:00
Flomp
32a9a67e7e Adds czech (#774)
Co-authored-by: Christian Beutel <>
2026-02-05 09:36:57 +01:00
dependabot[bot]
df55864a6c Bump canvg in /web (#773)
Bumps [canvg](https://github.com/canvg/canvg) to 4.0.3 and updates ancestor dependency . These dependencies need to be updated together.


Updates `canvg` from 4.0.2 to 4.0.3
- [Release notes](https://github.com/canvg/canvg/releases)
- [Changelog](https://github.com/canvg/canvg/blob/master/CHANGELOG.md)
- [Commits](https://github.com/canvg/canvg/compare/v4.0.2...v4.0.3)

Updates `canvg` from 3.0.10 to 3.0.11
- [Release notes](https://github.com/canvg/canvg/releases)
- [Changelog](https://github.com/canvg/canvg/blob/master/CHANGELOG.md)
- [Commits](https://github.com/canvg/canvg/compare/v4.0.2...v4.0.3)

---
updated-dependencies:
- dependency-name: canvg
  dependency-version: 4.0.3
  dependency-type: direct:production
- dependency-name: canvg
  dependency-version: 3.0.11
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-05 09:31:09 +01:00
dependabot[bot]
7643bd11e4 Bump devalue from 5.1.1 to 5.6.2 in /web (#732)
Bumps [devalue](https://github.com/sveltejs/devalue) from 5.1.1 to 5.6.2.
- [Release notes](https://github.com/sveltejs/devalue/releases)
- [Changelog](https://github.com/sveltejs/devalue/blob/main/CHANGELOG.md)
- [Commits](https://github.com/sveltejs/devalue/compare/v5.1.1...v5.6.2)

---
updated-dependencies:
- dependency-name: devalue
  dependency-version: 5.6.2
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-05 09:16:01 +01:00
dependabot[bot]
a4fcbb21fd Bump tar from 7.4.3 to 7.5.7 in /web (#754)
Bumps [tar](https://github.com/isaacs/node-tar) from 7.4.3 to 7.5.7.
- [Release notes](https://github.com/isaacs/node-tar/releases)
- [Changelog](https://github.com/isaacs/node-tar/blob/main/CHANGELOG.md)
- [Commits](https://github.com/isaacs/node-tar/compare/v7.4.3...v7.5.7)

---
updated-dependencies:
- dependency-name: tar
  dependency-version: 7.5.7
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-05 08:58:35 +01:00
dependabot[bot]
7c7c5f89fb Bump brace-expansion from 2.0.1 to 2.0.2 in /web (#711)
Bumps [brace-expansion](https://github.com/juliangruber/brace-expansion) from 2.0.1 to 2.0.2.
- [Release notes](https://github.com/juliangruber/brace-expansion/releases)
- [Commits](https://github.com/juliangruber/brace-expansion/compare/v2.0.1...v2.0.2)

---
updated-dependencies:
- dependency-name: brace-expansion
  dependency-version: 2.0.2
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-05 08:51:47 +01:00
dependabot[bot]
7c5d6012dd Bump vite from 5.4.14 to 5.4.21 in /web (#714)
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 5.4.14 to 5.4.21.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/v5.4.21/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v5.4.21/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-version: 5.4.21
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-05 08:51:27 +01:00
dependabot[bot]
f864b55019 Bump linkifyjs from 4.3.1 to 4.3.2 in /web (#712)
Bumps [linkifyjs](https://github.com/nfrasser/linkifyjs/tree/HEAD/packages/linkifyjs) from 4.3.1 to 4.3.2.
- [Release notes](https://github.com/nfrasser/linkifyjs/releases)
- [Changelog](https://github.com/nfrasser/linkifyjs/blob/main/CHANGELOG.md)
- [Commits](https://github.com/nfrasser/linkifyjs/commits/v4.3.2/packages/linkifyjs)

---
updated-dependencies:
- dependency-name: linkifyjs
  dependency-version: 4.3.2
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-05 08:51:04 +01:00
dependabot[bot]
f82e064ce8 Bump the docker-backward-compatible group in /docs with 10 updates (#757)
Bumps the docker-backward-compatible group in /docs with 10 updates:

| Package | From | To |
| --- | --- | --- |
| [@astrojs/check](https://github.com/withastro/astro/tree/HEAD/packages/language-tools/astro-check) | `0.9.5` | `0.9.6` |
| [@astrojs/node](https://github.com/withastro/astro/tree/HEAD/packages/integrations/node) | `9.5.0` | `9.5.2` |
| [@astrojs/starlight](https://github.com/withastro/starlight/tree/HEAD/packages/starlight) | `0.36.2` | `0.37.5` |
| [@astrojs/svelte](https://github.com/withastro/astro/tree/HEAD/packages/integrations/svelte) | `7.2.1` | `7.2.5` |
| [@tailwindcss/vite](https://github.com/tailwindlabs/tailwindcss/tree/HEAD/packages/@tailwindcss-vite) | `4.1.16` | `4.1.18` |
| [astro](https://github.com/withastro/astro/tree/HEAD/packages/astro) | `5.15.3` | `5.17.1` |
| [sharp](https://github.com/lovell/sharp) | `0.34.4` | `0.34.5` |
| [starlight-openapi](https://github.com/HiDeoo/starlight-openapi/tree/HEAD/packages/starlight-openapi) | `0.21.1` | `0.22.0` |
| [svelte](https://github.com/sveltejs/svelte/tree/HEAD/packages/svelte) | `5.43.2` | `5.49.1` |
| [tailwindcss](https://github.com/tailwindlabs/tailwindcss/tree/HEAD/packages/tailwindcss) | `4.1.16` | `4.1.18` |


Updates `@astrojs/check` from 0.9.5 to 0.9.6
- [Release notes](https://github.com/withastro/astro/releases)
- [Changelog](https://github.com/withastro/astro/blob/main/packages/language-tools/astro-check/CHANGELOG.md)
- [Commits](https://github.com/withastro/astro/commits/@astrojs/check@0.9.6/packages/language-tools/astro-check)

Updates `@astrojs/node` from 9.5.0 to 9.5.2
- [Release notes](https://github.com/withastro/astro/releases)
- [Changelog](https://github.com/withastro/astro/blob/main/packages/integrations/node/CHANGELOG.md)
- [Commits](https://github.com/withastro/astro/commits/@astrojs/node@9.5.2/packages/integrations/node)

Updates `@astrojs/starlight` from 0.36.2 to 0.37.5
- [Release notes](https://github.com/withastro/starlight/releases)
- [Changelog](https://github.com/withastro/starlight/blob/main/packages/starlight/CHANGELOG.md)
- [Commits](https://github.com/withastro/starlight/commits/@astrojs/starlight@0.37.5/packages/starlight)

Updates `@astrojs/svelte` from 7.2.1 to 7.2.5
- [Release notes](https://github.com/withastro/astro/releases)
- [Changelog](https://github.com/withastro/astro/blob/main/packages/integrations/svelte/CHANGELOG.md)
- [Commits](https://github.com/withastro/astro/commits/@astrojs/svelte@7.2.5/packages/integrations/svelte)

Updates `@tailwindcss/vite` from 4.1.16 to 4.1.18
- [Release notes](https://github.com/tailwindlabs/tailwindcss/releases)
- [Changelog](https://github.com/tailwindlabs/tailwindcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/tailwindlabs/tailwindcss/commits/v4.1.18/packages/@tailwindcss-vite)

Updates `astro` from 5.15.3 to 5.17.1
- [Release notes](https://github.com/withastro/astro/releases)
- [Changelog](https://github.com/withastro/astro/blob/main/packages/astro/CHANGELOG.md)
- [Commits](https://github.com/withastro/astro/commits/astro@5.17.1/packages/astro)

Updates `sharp` from 0.34.4 to 0.34.5
- [Release notes](https://github.com/lovell/sharp/releases)
- [Commits](https://github.com/lovell/sharp/compare/v0.34.4...v0.34.5)

Updates `starlight-openapi` from 0.21.1 to 0.22.0
- [Release notes](https://github.com/HiDeoo/starlight-openapi/releases)
- [Changelog](https://github.com/HiDeoo/starlight-openapi/blob/main/packages/starlight-openapi/CHANGELOG.md)
- [Commits](https://github.com/HiDeoo/starlight-openapi/commits/starlight-openapi@0.22.0/packages/starlight-openapi)

Updates `svelte` from 5.43.2 to 5.49.1
- [Release notes](https://github.com/sveltejs/svelte/releases)
- [Changelog](https://github.com/sveltejs/svelte/blob/main/packages/svelte/CHANGELOG.md)
- [Commits](https://github.com/sveltejs/svelte/commits/svelte@5.49.1/packages/svelte)

Updates `tailwindcss` from 4.1.16 to 4.1.18
- [Release notes](https://github.com/tailwindlabs/tailwindcss/releases)
- [Changelog](https://github.com/tailwindlabs/tailwindcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/tailwindlabs/tailwindcss/commits/v4.1.18/packages/tailwindcss)

---
updated-dependencies:
- dependency-name: "@astrojs/check"
  dependency-version: 0.9.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: docker-backward-compatible
- dependency-name: "@astrojs/node"
  dependency-version: 9.5.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: docker-backward-compatible
- dependency-name: "@astrojs/starlight"
  dependency-version: 0.37.5
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: docker-backward-compatible
- dependency-name: "@astrojs/svelte"
  dependency-version: 7.2.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: docker-backward-compatible
- dependency-name: "@tailwindcss/vite"
  dependency-version: 4.1.18
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: docker-backward-compatible
- dependency-name: astro
  dependency-version: 5.17.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: docker-backward-compatible
- dependency-name: sharp
  dependency-version: 0.34.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: docker-backward-compatible
- dependency-name: starlight-openapi
  dependency-version: 0.22.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: docker-backward-compatible
- dependency-name: svelte
  dependency-version: 5.49.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: docker-backward-compatible
- dependency-name: tailwindcss
  dependency-version: 4.1.18
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: docker-backward-compatible
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-05 08:48:53 +01:00
dependabot[bot]
357b446bfd Bump golang.org/x/net in /db in the gomod-backward-compatible group (#756)
Bumps the gomod-backward-compatible group in /db with 1 update: [golang.org/x/net](https://github.com/golang/net).


Updates `golang.org/x/net` from 0.48.0 to 0.49.0
- [Commits](https://github.com/golang/net/compare/v0.48.0...v0.49.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-version: 0.49.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: gomod-backward-compatible
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-05 08:48:20 +01:00
dependabot[bot]
7224e41930 Bump @fortawesome/fontawesome-free from 6.7.2 to 7.0.0 in /web (#493)
Bumps [@fortawesome/fontawesome-free](https://github.com/FortAwesome/Font-Awesome) from 6.7.2 to 7.0.0.
- [Release notes](https://github.com/FortAwesome/Font-Awesome/releases)
- [Changelog](https://github.com/FortAwesome/Font-Awesome/blob/7.x/CHANGELOG.md)
- [Commits](https://github.com/FortAwesome/Font-Awesome/compare/6.7.2...7.0.0)

---
updated-dependencies:
- dependency-name: "@fortawesome/fontawesome-free"
  dependency-version: 7.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-05 08:47:54 +01:00
dependabot[bot]
77dcac7808 Bump playwright and @playwright/test in /web (#713)
Bumps [playwright](https://github.com/microsoft/playwright) to 1.57.0 and updates ancestor dependency [@playwright/test](https://github.com/microsoft/playwright). These dependencies need to be updated together.


Updates `playwright` from 1.49.1 to 1.57.0
- [Release notes](https://github.com/microsoft/playwright/releases)
- [Commits](https://github.com/microsoft/playwright/compare/v1.49.1...v1.57.0)

Updates `@playwright/test` from 1.49.1 to 1.57.0
- [Release notes](https://github.com/microsoft/playwright/releases)
- [Commits](https://github.com/microsoft/playwright/compare/v1.49.1...v1.57.0)

---
updated-dependencies:
- dependency-name: playwright
  dependency-version: 1.57.0
  dependency-type: indirect
- dependency-name: "@playwright/test"
  dependency-version: 1.57.0
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-05 08:47:28 +01:00
Jonas Plum
359957017f Enable integration tests (#760)
* Enable integration tests

* Install chromium

* Enable webServer

* Run docker compose

* Build docker images

* Set working dir

* Add Makefile

* Fix healthcheck
2026-02-05 08:40:26 +01:00
Flomp
32e6b6a6cc fix/pocketbase-smtp-sender-address-spelling (#769)
Co-authored-by: Christian Beutel <>
2026-02-04 11:26:49 +01:00
Flomp
44fbaa5e27 fix/map-state-amenity-spelling (#768)
Co-authored-by: Christian Beutel <>
2026-02-04 11:20:02 +01:00
github-actions[bot]
9db829a09f chore: release v0.18.4 (#764)
Co-authored-by: Flomp <26000991+Flomp@users.noreply.github.com>
2026-02-03 15:22:30 +01:00
Flomp
27728c4ce7 fix/release-workflow (#763)
* fix/release-workflow

* Potential fix for code scanning alert no. 20: Workflow does not contain permissions

Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>

---------

Co-authored-by: Christian Beutel <>
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
2026-02-03 15:14:55 +01:00
Flomp
f99c650f8b v0.18.4 Changelog (#761)
* initial commit

* update changelog

---------

Co-authored-by: Christian Beutel <>
2026-02-02 18:24:45 +01:00
Flomp
dacdaeab26 New Crowdin updates (#622)
* New translations en.json (French)

* New translations en.json (Dutch)

* New translations en.json (Dutch)

* New translations en.json (Russian)

* New translations en.json (Russian)

* New translations en.json (French)

* New translations en.json (French)

* New translations en.json (Spanish)

* New translations en.json (Spanish)

* New translations en.json (Czech)

* New translations en.json (Czech)

* New translations en.json (Czech)

* New translations en.json (Czech)

* New translations en.json (Czech)

* New translations en.json (Czech)

* New translations en.json (Czech)

* New translations en.json (Czech)

* New translations en.json (Czech)

* New translations en.json (Czech)

* New translations en.json (Czech)

* New translations en.json (Czech)

* New translations en.json (German)

* New translations en.json (French)

* New translations en.json (German)

* New translations en.json (German)

* New translations en.json (German)

* New translations en.json (German)

* fixes valhalla spelling

* Added Norwegian translation (#668)

* adds norwegian

* minor fixes in de

---------

Co-authored-by: Christian Beutel <>
Co-authored-by: Pål Håland <4449863+palhaland@users.noreply.github.com>
2026-02-02 18:24:20 +01:00
slothful-vassal
6d5494935d fix playwright tests (#759) 2026-02-01 21:02:36 +01:00
Robert Clarke
029f1f134b Return 401s when logged out of more activitypub api endpoints (#750)
Co-authored-by: slothful-vassal <89943360+slothful-vassal@users.noreply.github.com>
2026-01-31 23:04:56 +01:00
Robert Clarke
1005aa0a84 add root CAs to db container (#751) 2026-01-25 18:06:49 +01:00
Patrick Stöckle
6318f4b4ad Improve container images (#749)
* 🔒 chore(security): improve container image security

* 👌 chore(review): add cron job to Dockerfile for periodic tasks
2026-01-24 22:21:19 +01:00
dependabot[bot]
7546abb122 Bump devalue from 5.4.2 to 5.6.2 in /docs (#728)
Bumps [devalue](https://github.com/sveltejs/devalue) from 5.4.2 to 5.6.2.
- [Release notes](https://github.com/sveltejs/devalue/releases)
- [Changelog](https://github.com/sveltejs/devalue/blob/main/CHANGELOG.md)
- [Commits](https://github.com/sveltejs/devalue/compare/v5.4.2...v5.6.2)

---
updated-dependencies:
- dependency-name: devalue
  dependency-version: 5.6.2
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-23 21:03:27 +01:00
dependabot[bot]
09c7f26db9 Bump h3 from 1.15.4 to 1.15.5 in /docs (#730)
Bumps [h3](https://github.com/h3js/h3) from 1.15.4 to 1.15.5.
- [Release notes](https://github.com/h3js/h3/releases)
- [Changelog](https://github.com/h3js/h3/blob/v1.15.5/CHANGELOG.md)
- [Commits](https://github.com/h3js/h3/compare/v1.15.4...v1.15.5)

---
updated-dependencies:
- dependency-name: h3
  dependency-version: 1.15.5
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-23 20:56:05 +01:00
dependabot[bot]
e685bc2e2a Bump diff from 5.2.0 to 5.2.2 in /docs (#744)
Bumps [diff](https://github.com/kpdecker/jsdiff) from 5.2.0 to 5.2.2.
- [Changelog](https://github.com/kpdecker/jsdiff/blob/master/release-notes.md)
- [Commits](https://github.com/kpdecker/jsdiff/compare/v5.2.0...v5.2.2)

---
updated-dependencies:
- dependency-name: diff
  dependency-version: 5.2.2
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-23 20:53:37 +01:00
Jonas Plum
b435e02cb9 Enforce npm check step (#724) 2026-01-13 20:28:54 +01:00
slothful-vassal
4b749c9868 Fix npm run check (#723)
* fix npm run check errors

* undo changes in style_switcher
2026-01-13 20:25:40 +01:00
Jonas Plum
c2b86f5521 Change repo url (#722) 2026-01-13 20:23:42 +01:00
Flomp
b65fbdd128 fix trail privacy (#719)
* fix trail privacy

* adds privacy setting to integrations

---------

Co-authored-by: Christian Beutel <>
2026-01-13 19:01:05 +01:00
slothful-vassal
c80dc01c7a fix category filter (#654) 2026-01-13 12:04:52 +01:00
slothful-vassal
57b37d84c4 Fix new trail (#673)
* Update FUNDING.yml

* fix new trail page reload
2026-01-13 11:57:27 +01:00
dependabot[bot]
839679074f Bump the gomod-backward-compatible group across 1 directory with 3 updates (#717)
Bumps the gomod-backward-compatible group with 2 updates in the /db directory: [github.com/spf13/cobra](https://github.com/spf13/cobra) and [golang.org/x/net](https://github.com/golang/net).


Updates `github.com/spf13/cobra` from 1.10.1 to 1.10.2
- [Release notes](https://github.com/spf13/cobra/releases)
- [Commits](https://github.com/spf13/cobra/compare/v1.10.1...v1.10.2)

Updates `golang.org/x/net` from 0.44.0 to 0.48.0
- [Commits](https://github.com/golang/net/compare/v0.44.0...v0.48.0)

Updates `golang.org/x/sync` from 0.17.0 to 0.19.0
- [Commits](https://github.com/golang/sync/compare/v0.17.0...v0.19.0)

---
updated-dependencies:
- dependency-name: github.com/spf13/cobra
  dependency-version: 1.10.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: gomod-backward-compatible
- dependency-name: golang.org/x/net
  dependency-version: 0.48.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: gomod-backward-compatible
- dependency-name: golang.org/x/sync
  dependency-version: 0.19.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: gomod-backward-compatible
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-12 06:32:34 +01:00
dependabot[bot]
e1764c128e Bump the docker-backward-compatible group across 1 directory with 14 updates (#640)
Bumps the docker-backward-compatible group with 12 updates in the /docs directory:

| Package | From | To |
| --- | --- | --- |
| [@astrojs/check](https://github.com/withastro/language-tools/tree/HEAD/packages/astro-check) | `0.9.4` | `0.9.5` |
| [@astrojs/node](https://github.com/withastro/astro/tree/HEAD/packages/integrations/node) | `9.2.2` | `9.5.0` |
| [@astrojs/starlight](https://github.com/withastro/starlight/tree/HEAD/packages/starlight) | `0.34.4` | `0.36.2` |
| [@astrojs/starlight-tailwind](https://github.com/withastro/starlight/tree/HEAD/packages/tailwind) | `4.0.1` | `4.0.2` |
| [@astrojs/svelte](https://github.com/withastro/astro/tree/HEAD/packages/integrations/svelte) | `7.1.0` | `7.2.1` |
| [@fontsource/ibm-plex-mono](https://github.com/fontsource/font-files/tree/HEAD/fonts/google/ibm-plex-mono) | `5.2.6` | `5.2.7` |
| [@fontsource/ibm-plex-sans](https://github.com/fontsource/font-files/tree/HEAD/fonts/google/ibm-plex-sans) | `5.2.6` | `5.2.8` |
| [@tailwindcss/vite](https://github.com/tailwindlabs/tailwindcss/tree/HEAD/packages/@tailwindcss-vite) | `4.1.10` | `4.1.16` |
| [sharp](https://github.com/lovell/sharp) | `0.32.6` | `0.34.4` |
| [starlight-openapi](https://github.com/HiDeoo/starlight-openapi/tree/HEAD/packages/starlight-openapi) | `0.9.0` | `0.21.1` |
| [svelte](https://github.com/sveltejs/svelte/tree/HEAD/packages/svelte) | `5.34.8` | `5.43.2` |
| [typescript](https://github.com/microsoft/TypeScript) | `5.8.3` | `5.9.3` |



Updates `@astrojs/check` from 0.9.4 to 0.9.5
- [Release notes](https://github.com/withastro/language-tools/releases)
- [Changelog](https://github.com/withastro/language-tools/blob/main/packages/astro-check/CHANGELOG.md)
- [Commits](https://github.com/withastro/language-tools/commits/HEAD/packages/astro-check)

Updates `@astrojs/node` from 9.2.2 to 9.5.0
- [Release notes](https://github.com/withastro/astro/releases)
- [Changelog](https://github.com/withastro/astro/blob/main/packages/integrations/node/CHANGELOG.md)
- [Commits](https://github.com/withastro/astro/commits/@astrojs/node@9.5.0/packages/integrations/node)

Updates `@astrojs/starlight` from 0.34.4 to 0.36.2
- [Release notes](https://github.com/withastro/starlight/releases)
- [Changelog](https://github.com/withastro/starlight/blob/main/packages/starlight/CHANGELOG.md)
- [Commits](https://github.com/withastro/starlight/commits/@astrojs/starlight@0.36.2/packages/starlight)

Updates `@astrojs/starlight-tailwind` from 4.0.1 to 4.0.2
- [Release notes](https://github.com/withastro/starlight/releases)
- [Changelog](https://github.com/withastro/starlight/blob/main/packages/tailwind/CHANGELOG.md)
- [Commits](https://github.com/withastro/starlight/commits/@astrojs/starlight-tailwind@4.0.2/packages/tailwind)

Updates `@astrojs/svelte` from 7.1.0 to 7.2.1
- [Release notes](https://github.com/withastro/astro/releases)
- [Changelog](https://github.com/withastro/astro/blob/main/packages/integrations/svelte/CHANGELOG.md)
- [Commits](https://github.com/withastro/astro/commits/@astrojs/svelte@7.2.1/packages/integrations/svelte)

Updates `@fontsource/ibm-plex-mono` from 5.2.6 to 5.2.7
- [Changelog](https://github.com/fontsource/font-files/blob/main/CHANGELOG.md)
- [Commits](https://github.com/fontsource/font-files/commits/HEAD/fonts/google/ibm-plex-mono)

Updates `@fontsource/ibm-plex-sans` from 5.2.6 to 5.2.8
- [Changelog](https://github.com/fontsource/font-files/blob/main/CHANGELOG.md)
- [Commits](https://github.com/fontsource/font-files/commits/HEAD/fonts/google/ibm-plex-sans)

Updates `@tailwindcss/vite` from 4.1.10 to 4.1.16
- [Release notes](https://github.com/tailwindlabs/tailwindcss/releases)
- [Changelog](https://github.com/tailwindlabs/tailwindcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/tailwindlabs/tailwindcss/commits/v4.1.16/packages/@tailwindcss-vite)

Updates `astro` from 5.9.3 to 5.15.3
- [Release notes](https://github.com/withastro/astro/releases)
- [Changelog](https://github.com/withastro/astro/blob/main/packages/astro/CHANGELOG.md)
- [Commits](https://github.com/withastro/astro/commits/astro@5.15.3/packages/astro)

Updates `sharp` from 0.32.6 to 0.34.4
- [Release notes](https://github.com/lovell/sharp/releases)
- [Commits](https://github.com/lovell/sharp/compare/v0.32.6...v0.34.4)

Updates `starlight-openapi` from 0.9.0 to 0.21.1
- [Release notes](https://github.com/HiDeoo/starlight-openapi/releases)
- [Changelog](https://github.com/HiDeoo/starlight-openapi/blob/main/packages/starlight-openapi/CHANGELOG.md)
- [Commits](https://github.com/HiDeoo/starlight-openapi/commits/starlight-openapi@0.21.1/packages/starlight-openapi)

Updates `svelte` from 5.34.8 to 5.43.2
- [Release notes](https://github.com/sveltejs/svelte/releases)
- [Changelog](https://github.com/sveltejs/svelte/blob/main/packages/svelte/CHANGELOG.md)
- [Commits](https://github.com/sveltejs/svelte/commits/svelte@5.43.2/packages/svelte)

Updates `tailwindcss` from 4.1.10 to 4.1.16
- [Release notes](https://github.com/tailwindlabs/tailwindcss/releases)
- [Changelog](https://github.com/tailwindlabs/tailwindcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/tailwindlabs/tailwindcss/commits/v4.1.16/packages/tailwindcss)

Updates `typescript` from 5.8.3 to 5.9.3
- [Release notes](https://github.com/microsoft/TypeScript/releases)
- [Changelog](https://github.com/microsoft/TypeScript/blob/main/azure-pipelines.release-publish.yml)
- [Commits](https://github.com/microsoft/TypeScript/compare/v5.8.3...v5.9.3)

---
updated-dependencies:
- dependency-name: "@astrojs/check"
  dependency-version: 0.9.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: docker-backward-compatible
- dependency-name: "@astrojs/node"
  dependency-version: 9.5.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: docker-backward-compatible
- dependency-name: "@astrojs/starlight"
  dependency-version: 0.36.2
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: docker-backward-compatible
- dependency-name: "@astrojs/starlight-tailwind"
  dependency-version: 4.0.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: docker-backward-compatible
- dependency-name: "@astrojs/svelte"
  dependency-version: 7.2.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: docker-backward-compatible
- dependency-name: "@fontsource/ibm-plex-mono"
  dependency-version: 5.2.7
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: docker-backward-compatible
- dependency-name: "@fontsource/ibm-plex-sans"
  dependency-version: 5.2.8
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: docker-backward-compatible
- dependency-name: "@tailwindcss/vite"
  dependency-version: 4.1.16
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: docker-backward-compatible
- dependency-name: astro
  dependency-version: 5.15.3
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: docker-backward-compatible
- dependency-name: sharp
  dependency-version: 0.34.4
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: docker-backward-compatible
- dependency-name: starlight-openapi
  dependency-version: 0.21.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: docker-backward-compatible
- dependency-name: svelte
  dependency-version: 5.43.2
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: docker-backward-compatible
- dependency-name: tailwindcss
  dependency-version: 4.1.16
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: docker-backward-compatible
- dependency-name: typescript
  dependency-version: 5.9.3
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: docker-backward-compatible
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-11 23:42:57 +01:00
dependabot[bot]
7fb82216fe Bump actions/checkout from 5 to 6 (#660)
Bumps [actions/checkout](https://github.com/actions/checkout) from 5 to 6.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-11 22:48:13 +01:00
dependabot[bot]
4eae263be1 Bump actions/setup-node from 4 to 6 (#638)
Bumps [actions/setup-node](https://github.com/actions/setup-node) from 4 to 6.
- [Release notes](https://github.com/actions/setup-node/releases)
- [Commits](https://github.com/actions/setup-node/compare/v4...v6)

---
updated-dependencies:
- dependency-name: actions/setup-node
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-11 22:47:03 +01:00
dependabot[bot]
9a50569bb3 Bump actions/setup-go from 5 to 6 (#612)
Bumps [actions/setup-go](https://github.com/actions/setup-go) from 5 to 6.
- [Release notes](https://github.com/actions/setup-go/releases)
- [Commits](https://github.com/actions/setup-go/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/setup-go
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-11 22:44:49 +01:00
slothful-vassal
86cca8e8c3 fix duplicated lists after publishing (activitypub instances) (#706) 2026-01-11 22:19:16 +01:00
slothful-vassal
6e5edf7ed7 fix lost gpx on summit log update (#702)
* fix lost gpx on summit log update

* fix missing trail in summit log modal
2026-01-11 22:13:39 +01:00
slothful-vassal
9282f37c15 fix list fetch timeout (#701)
* fix list fetch timeout

* list search modal added (replaces list select modal)

* query startsWith -> includes

---------

Co-authored-by: Christian Beutel <>
2026-01-11 22:03:35 +01:00
Henri
741cb51096 Fix swapped options for auto and bicycle routing (#694) 2026-01-11 21:44:39 +01:00
Niklas
712174491b fix spelling so translating is easier (#700)
Co-authored-by: Niklas <niklas1375@gmail.com>
2026-01-11 21:27:50 +01:00
Jonas Plum
555ecfdc5e add a docker-compose file for production deployments 2026-01-11 20:19:56 +01:00
Christian Beutel
81d148c09f fixes tag delete 2025-10-05 13:15:19 +02:00
Christian Beutel
237225f10b block external profiles for unregistered users 2025-10-05 12:46:02 +02:00
Christian Beutel
84d9f2483e block external profiles for unregistered users 2025-10-05 12:42:20 +02:00
Jonas Plum
8bf4138962 Add setup.sh script (#595)
* Add setup.sh script

* Remove update menu item

* make setup.sh work on macOS

* Update wording

---------

Co-authored-by: Christian Beutel <>
2025-10-05 10:14:20 +02:00
slothful-vassal
b09675f2b2 Select number of search items per page (#577)
* select items per pages

* fix 'all'

* fix page switch

* fix filter update when 'all'

* startDrawing: set map to current user position

* remove startDrawing change from other branch

* handle 413

* remove 'all', cange increment, use local storage

* remove obsolete import, fix 413
2025-10-04 17:22:46 +02:00
slothful-vassal
7ed0b98fce fix scrollbar theme in chrome (#593) 2025-10-04 14:50:26 +02:00
slothful-vassal
44bb49ec96 speed up POI retrieval by custom overpass api url (#596)
* speed up POI retrieval by custom overpass api url

* consistent overpass api url default

* switches default to overpass-api.de

---------

Co-authored-by: Christian Beutel <>
2025-10-04 14:46:52 +02:00
Flomp
23dd744b28 New Crowdin updates (#607)
* New translations en.json (Basque)

* New translations en.json (Basque)
2025-10-04 14:38:45 +02:00
slothful-vassal
03ef547a75 improve some elements for dark mode (#609) 2025-10-04 14:38:27 +02:00
Robert Clarke
d65d812d4d FIx html entities in waypoint descriptions in print PDF (#615)
* fix html entities in waypoints in generated PDF

* fix padding and text heights
2025-10-04 14:36:23 +02:00
Christian Beutel
e910b3c44d removes deprecated fsys call 2025-09-19 18:21:15 +02:00
github-actions[bot]
2edba0549a Release 0.18.3 2025-09-19 16:08:07 +00:00
Christian Beutel
722bd6c078 updates changelog 2025-09-19 18:07:27 +02:00
Christian Beutel
4010b85442 fixes show on map from feed 2025-09-19 18:03:16 +02:00
Christian Beutel
3ced9e0dd6 Merge remote-tracking branch 'origin/i10n_main' 2025-09-19 17:54:02 +02:00
Christian Beutel
e6f47b348c possible fix for release workflow db image 2025-09-19 17:53:17 +02:00
Christian Beutel
1632bc4f34 update pocketbase to 0.30.0 2025-09-19 15:47:43 +02:00
wolffshots
149157604d Fix oidc2 and oidc3 logos (#564)
* chore: strip numbers form oidc providers

* chore: strip only trailing numbers

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-09-19 15:21:17 +02:00
Christian Beutel
cf74a711e9 Fixes response for protected API routes 2025-09-19 15:11:09 +02:00
Christian Beutel
5580832675 fixes waypoint update/delete 2025-09-19 14:02:03 +02:00
Christian Beutel
411bb3cc45 update server list 2025-09-18 16:37:02 +02:00
Christian Beutel
378f030b4a fixes waypoint creation for exitsing trails 2025-09-18 15:51:00 +02:00
Flomp
97abc9f3d0 New translations en.json (Russian) 2025-09-17 14:59:44 +02:00
Flomp
2cb02c0236 New translations en.json (German) 2025-09-15 00:52:43 +02:00
Flomp
88406ee175 New translations en.json (German) 2025-09-14 23:55:09 +02:00
Flomp
72b57f85bd New translations en.json (Chinese Simplified) 2025-09-13 16:53:22 +02:00
Flomp
e9422efc5d New translations en.json (Russian) 2025-09-13 16:53:21 +02:00
Flomp
fbd61774dc New translations en.json (Portuguese) 2025-09-13 16:53:20 +02:00
Flomp
678dacb1a1 New translations en.json (Polish) 2025-09-13 16:53:19 +02:00
Flomp
d736e3b500 New translations en.json (Dutch) 2025-09-13 16:53:18 +02:00
Flomp
f627341f1c New translations en.json (Italian) 2025-09-13 16:53:17 +02:00
Flomp
cef7c3ac21 New translations en.json (Hungarian) 2025-09-13 16:53:16 +02:00
Flomp
4d6b557ed0 New translations en.json (Basque) 2025-09-13 16:53:15 +02:00
Flomp
12ea2addfb New translations en.json (German) 2025-09-13 16:53:14 +02:00
Flomp
4c6990e7a4 New translations en.json (Spanish) 2025-09-13 16:53:13 +02:00
Flomp
3d37d51b0b New translations en.json (French) 2025-09-13 16:53:12 +02:00
github-actions[bot]
b4ff4795bc Release 0.18.2 2025-09-13 14:41:41 +00:00
Christian Beutel
ca477984cc Merge branch 'main' into i10n_main 2025-09-13 16:40:14 +02:00
Christian Beutel
adf41c370b updates changelog 2025-09-13 16:39:37 +02:00
Christian Beutel
72acac072c fixes strava integration 2025-09-13 16:04:48 +02:00
Christian Beutel
962fddc1e2 adds dedup command 2025-09-13 13:27:23 +02:00
Christian Beutel
24908993fa adds index to activity_actors table 2025-09-13 12:36:22 +02:00
Christian Beutel
b2be48a282 improves initial meilisearch indexing speed 2025-09-13 11:33:19 +02:00
Christian Beutel
73d20b1cbc reverses waypoint relation 2025-09-12 17:48:41 +02:00
Christian Beutel
9c628c6511 cache public meilisearch key 2025-09-12 16:45:50 +02:00
Christian Beutel
3b7f30908c fixes sorting by difficulty 2025-09-07 17:59:35 +02:00
slothful-vassal
8cc7029243 fix opening trail from map search (#562) 2025-09-07 16:34:25 +02:00
Christian Beutel
5174663104 fixes link to new trail on empty profiles 2025-09-07 16:32:28 +02:00
Christian Beutel
64579808a9 fixes duplicate trails 2025-08-31 16:41:03 +02:00
Christian Beutel
c6042db9a8 fixes permission issues for non-public trails 2025-08-29 15:18:44 +02:00
Jonas Plum
16458dc365 Merge pull request #495 from Flomp/dependabot/github_actions/actions/checkout-5
Bump actions/checkout from 3 to 5
2025-08-28 20:18:10 +02:00
Jonas Plum
b734c325ee Merge pull request #492 from Flomp/dependabot/github_actions/actions/setup-node-4
Bump actions/setup-node from 3 to 4
2025-08-28 20:17:59 +02:00
Jonas Plum
64320ab31c Merge pull request #487 from Flomp/037adependabot/github_actions/actions/setup-go-5
Bump actions/setup-go from 4 to 5
2025-08-28 20:17:40 +02:00
Flomp
9dfbd106af New translations en.json (Russian) 2025-08-28 15:50:35 +02:00
Flomp
625c5e302d New translations en.json (Basque) 2025-08-28 15:50:31 +02:00
Flomp
5dcc7d057b New translations en.json (German) 2025-08-28 15:50:30 +02:00
github-actions[bot]
39f33f1eb2 Release 0.18.1 2025-08-28 13:05:14 +00:00
Christian Beutel
4ffd2dd0c2 updates changelog 2025-08-28 15:04:21 +02:00
Christian Beutel
7807c5c7c2 fix bare-metal update instructions 2025-08-28 14:53:29 +02:00
dependabot[bot]
66e482c6fc Bump actions/checkout from 3 to 5
Bumps [actions/checkout](https://github.com/actions/checkout) from 3 to 5.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v3...v5)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-28 12:36:05 +00:00
Flomp
0032556cb9 New Crowdin updates (#478)
* New translations en.json (French)

* New translations en.json (Spanish)

* New translations en.json (German)

* New translations en.json (Basque)

* New translations en.json (Hungarian)

* New translations en.json (Italian)

* New translations en.json (Dutch)

* New translations en.json (Polish)

* New translations en.json (Portuguese)

* New translations en.json (Russian)

* New translations en.json (Chinese Simplified)

---------

Co-authored-by: Christian Beutel <>
2025-08-28 14:33:39 +02:00
dependabot[bot]
9597435dff Bump actions/setup-node from 3 to 4
Bumps [actions/setup-node](https://github.com/actions/setup-node) from 3 to 4.
- [Release notes](https://github.com/actions/setup-node/releases)
- [Commits](https://github.com/actions/setup-node/compare/v3...v4)

---
updated-dependencies:
- dependency-name: actions/setup-node
  dependency-version: '4'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-28 12:17:46 +00:00
Christian Beutel
432176d8f5 fixes list saving 2025-08-28 14:10:04 +02:00
dependabot[bot]
6b0bdcfff6 Bump actions/setup-go from 4 to 5
Bumps [actions/setup-go](https://github.com/actions/setup-go) from 4 to 5.
- [Release notes](https://github.com/actions/setup-go/releases)
- [Commits](https://github.com/actions/setup-go/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/setup-go
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-28 11:51:45 +00:00
Eric Daigle
c867fd7319 Add header to searchLocationReverse (#458) (#485)
As described in #458, uploading trails intermittently fails because the request to the Nominatim API in `searchLocationReverse` does not
contain a header as required by the API. The header was added to `searchLocations` in 47f431e, but the issue with the trail upload was
caused by it missing in `searchLocationReverse`. This PR adds it there as well.

Co-authored-by: Eric Daigle <eric.daigle@mailbox.org>
2025-08-28 13:36:13 +02:00
Jonas Plum
c0116ebbec Add dependabot version updates (#468) 2025-08-28 13:33:41 +02:00
Jonas Plum
1f42b6c85b Add issue templates (#469)
* Add issue templates

* Move feature requests to discussions
2025-08-28 13:32:08 +02:00
Christian Beutel
a8de5d075f adds es, eu, ru translations 2025-08-24 15:23:54 +02:00
Christian Beutel
a00d92e93b fixes actor discovery 2025-08-24 14:59:50 +02:00
Christian Beutel
7596a916f8 Fixes category in lists 2025-08-24 14:59:50 +02:00
Christian Beutel
7f82509f3d Adds new server 2025-08-24 14:59:50 +02:00
github-actions[bot]
2502836d13 Release 0.18.0 2025-08-24 11:41:00 +00:00
Flomp
00bb717def New Crowdin updates (#394)
* New translations en.json (Spanish)

* New translations en.json (French)

* New translations en.json (German)

* New translations en.json (Hungarian)

* New translations en.json (Italian)

* New translations en.json (Dutch)

* New translations en.json (Polish)

* New translations en.json (Portuguese)

* New translations en.json (Chinese Simplified)

* New translations en.json (Russian)

* New translations en.json (Basque)

* New translations en.json (Spanish)

* New translations en.json (French)

* New translations en.json (German)

* New translations en.json (Hungarian)

* New translations en.json (Italian)

* New translations en.json (Dutch)

* New translations en.json (Polish)

* New translations en.json (Portuguese)

* New translations en.json (Chinese Simplified)

* New translations en.json (Russian)

* New translations en.json (Basque)

* New translations en.json (Spanish)

* New translations en.json (French)

* New translations en.json (German)

* New translations en.json (Hungarian)

* New translations en.json (Italian)

* New translations en.json (Dutch)

* New translations en.json (Polish)

* New translations en.json (Portuguese)

* New translations en.json (Chinese Simplified)

* New translations en.json (Russian)

* New translations en.json (Basque)

* New translations en.json (Spanish)

* New translations en.json (French)

* New translations en.json (German)

* New translations en.json (Hungarian)

* New translations en.json (Italian)

* New translations en.json (Dutch)

* New translations en.json (Polish)

* New translations en.json (Portuguese)

* New translations en.json (Chinese Simplified)

* New translations en.json (Russian)

* New translations en.json (Basque)

* New translations en.json (Russian)

* New translations en.json (French)

* New translations en.json (French)

* New translations en.json (Spanish)

* New translations en.json (French)

* New translations en.json (German)

* New translations en.json (Hungarian)

* New translations en.json (Italian)

* New translations en.json (Dutch)

* New translations en.json (Polish)

* New translations en.json (Portuguese)

* New translations en.json (Chinese Simplified)

* New translations en.json (Russian)

* New translations en.json (Basque)

* New translations en.json (Spanish)

* New translations en.json (French)

* New translations en.json (German)

* New translations en.json (Hungarian)

* New translations en.json (Italian)

* New translations en.json (Dutch)

* New translations en.json (Polish)

* New translations en.json (Portuguese)

* New translations en.json (Chinese Simplified)

* New translations en.json (Russian)

* New translations en.json (Basque)

* New translations en.json (Italian)

* New translations en.json (Basque)

* New translations en.json (Basque)

* New translations en.json (Basque)

* New translations en.json (Basque)

* New translations en.json (Chinese Simplified)

* New translations en.json (Chinese Simplified)

* New translations en.json (Chinese Simplified)

* New translations en.json (Chinese Simplified)

* New translations en.json (Basque)

* New translations en.json (French)

* New translations en.json (Spanish)

* New translations en.json (German)

* New translations en.json (Basque)

* New translations en.json (Hungarian)

* New translations en.json (Italian)

* New translations en.json (Dutch)

* New translations en.json (Polish)

* New translations en.json (Portuguese)

* New translations en.json (Russian)

* New translations en.json (Chinese Simplified)

* New translations en.json (German)

* New translations en.json (French)

* New translations en.json (Spanish)

* New translations en.json (German)

* New translations en.json (Basque)

* New translations en.json (Hungarian)

* New translations en.json (Italian)

* New translations en.json (Dutch)

* New translations en.json (Polish)

* New translations en.json (Portuguese)

* New translations en.json (Russian)

* New translations en.json (Chinese Simplified)

* adds OpenHikingMaps

* update changelogs

---------

Co-authored-by: Christian Beutel <>
2025-08-24 13:39:28 +02:00
Christian Beutel
7cadd08cb0 adds docs for "About" section 2025-08-24 12:20:27 +02:00
Christian Beutel
a02c6698c5 update db alpine version 2025-08-24 11:19:55 +02:00
Jonas Plum
98d3a90546 Add db CI (#472)
* Add Go CI

* Adjust trigger

* Adjust trigger
2025-08-24 10:49:59 +02:00
Jonas Plum
c904e3d34e Add web CI (#473) 2025-08-24 10:49:44 +02:00
Christian Beutel
4ba0281e0f adds customizable "about" section 2025-08-23 19:10:46 +02:00
Christian Beutel
5205d721a0 only auto upload if credentials provided 2025-08-23 15:07:27 +02:00
Christian Beutel
2aad94058a remove relatime notification subscription 2025-08-23 14:56:25 +02:00
Christian Beutel
089ce933dd adds caret layer for preview layer 2025-08-23 14:47:44 +02:00
slothful-vassal
056805f6a2 fix build dependencies (#459) 2025-08-23 14:47:27 +02:00
Jonas Plum
f73a109866 Verifiy settings more strictly (#433)
* Verifiy settings more strictly

* Warn only for usage of default keys
2025-08-23 14:40:39 +02:00
Jonas Plum
c93ab89f51 Break long trail titles (#466) 2025-08-23 14:35:03 +02:00
Christian Beutel
1cc70c4e4b adds polyline simplyfication 2025-08-23 14:28:00 +02:00
Christian Beutel
47f431e18d add header to nominatim response 2025-08-23 12:48:05 +02:00
Jonas Plum
05ad5ce8e0 Change docs (#441)
* Change docs

* hide backend config & small style change

---------

Co-authored-by: Christian Beutel <>
2025-08-22 17:56:38 +02:00
Christian Beutel
fdb6ef3b76 adds category to trail card/item/feed 2025-08-22 17:19:12 +02:00
Christian Beutel
380c1802f2 adds trail preview on high map zoom 2025-08-22 15:41:34 +02:00
Christian Beutel
33776ad42c fixes route drawing 2025-08-22 13:44:37 +02:00
Christian Beutel
e375747422 fixes profile discovery 2025-08-22 12:32:37 +02:00
Jonas Plum
f9451d73bf Add healthchecks (#437) 2025-08-22 10:53:55 +02:00
Christian Beutel
1e22c78c22 adds trail link sharing 2025-08-20 16:51:33 +02:00
Christian Beutel
b3d44f3b78 directions via osm 2025-08-20 14:50:01 +02:00
Christian Beutel
bdfaedb5fd fixes performance issues for private actors 2025-08-20 14:49:51 +02:00
Christian Beutel
097ecd409d correct elevation for summit logs 2025-08-20 13:35:08 +02:00
slothful-vassal
e3916e371a prevent uploading track details when updating a list (#460) 2025-08-20 13:26:19 +02:00
Mario Angst
c47654cfa2 Fixed small typo environment-configuration.md (#411) 2025-08-20 13:14:51 +02:00
Jonas Plum
b1173d52ae Fix typos (#438) 2025-08-04 19:23:14 +02:00
Christian Beutel
ef4bc9c961 reset pagination on filter update 2025-08-04 19:16:42 +02:00
Christian Beutel
482d90c893 allows categories with spaces in filter 2025-08-04 19:04:10 +02:00
Christian Beutel
b7c06fcaf2 switches to layer manager 2025-08-04 18:01:26 +02:00
Christian Beutel
53508be008 fixes trail saving 2025-08-02 22:19:11 +02:00
Christian Beutel
1c10e90db7 speed improvements for map (tbc) 2025-08-02 22:02:59 +02:00
Christian Beutel
396e64b5a6 fixes race condition in route edit menu 2025-08-02 18:35:50 +02:00
Christian Beutel
46d3c45b58 adds undo/redo to route edit 2025-08-02 17:00:09 +02:00
Christian Beutel
353e52b0f6 fixes split gpx function 2025-07-20 13:40:18 +02:00
Christian Beutel
634c34ce65 adds split gpx function 2025-07-20 11:35:21 +02:00
Christian Beutel
731a4ffb97 fixes crop icons 2025-07-19 20:02:47 +02:00
Christian Beutel
ef1571c54e adds route edit menu 2025-07-19 19:40:11 +02:00
Christian Beutel
6a908a3ace adds overpass icons 2025-07-18 21:42:16 +02:00
Christian Beutel
f46f8055c4 fixes POI overlay 2025-07-18 20:02:06 +02:00
Christian Beutel
eac9b2e489 adds layer manager 2025-07-13 22:22:08 +02:00
Christian Beutel
baa7bb4ecf add overlay layers 2025-07-12 20:37:51 +02:00
Christian Beutel
687fc0e1f6 limits feed to public items 2025-07-12 16:58:45 +02:00
Christian Beutel
03bf5aa5ad finishes feed 2025-07-12 16:36:40 +02:00
Christian Beutel
e8c7e7721a fixes feed 2025-07-11 17:13:30 +02:00
Christian Beutel
237d12bfec adds feed 2025-07-11 16:02:18 +02:00
Christian Beutel
25bf00a38b fixes timeline fetch 2025-07-11 12:12:42 +02:00
github-actions[bot]
2aaa3537a2 Release 0.17.2 2025-07-10 16:59:58 +00:00
Christian Beutel
620d724972 updates changelog 2025-07-10 18:59:18 +02:00
Christian Beutel
efeafe7222 Merge remote-tracking branch 'origin/i10n_main' 2025-07-10 18:57:45 +02:00
James Geiger
783e2cf233 Implement localization in calendar component (#386)
* Updates calendar component to use localized months/weekdays

Leverages svelte-i18n date formatter to localize the month name of the calendar.

For weekdays, to remain consistent with the two-letter format, implement custom i18n locale keys.

* Adds locale translations for weekday abbreviations in en and de
2025-07-10 18:55:38 +02:00
Christian Beutel
dc7afffe8a updates changelog 2025-07-10 18:52:27 +02:00
Christian Beutel
7cfca84ee3 updates server list 2025-07-10 18:52:21 +02:00
Christian Beutel
d21df88be2 adds trail sort on map 2025-07-10 17:53:29 +02:00
Christian Beutel
68f36dbf0c allow external URLs in private instances 2025-07-10 15:29:07 +02:00
Christian Beutel
d6e08e6d80 makes PostActivity fully async 2025-07-10 14:46:12 +02:00
Christian Beutel
d577ac209e fixes trail indexing for summit logs 2025-07-10 14:45:53 +02:00
Christian Beutel
95377f03c3 fixes hidden trail after map style switch 2025-07-10 13:45:07 +02:00
Flomp
612176b1ad New translations en.json (Basque) 2025-07-10 13:17:08 +02:00
Flomp
c8dc4135a6 New translations en.json (Russian) 2025-07-10 13:17:07 +02:00
Flomp
7791acb1fe New translations en.json (Chinese Simplified) 2025-07-10 13:17:06 +02:00
Flomp
6035c3933a New translations en.json (Portuguese) 2025-07-10 13:17:05 +02:00
Flomp
191a5f3342 New translations en.json (Polish) 2025-07-10 13:17:04 +02:00
Flomp
92e4a3c75e New translations en.json (Dutch) 2025-07-10 13:17:03 +02:00
Flomp
b96da13834 New translations en.json (Italian) 2025-07-10 13:17:02 +02:00
Flomp
f15633177a New translations en.json (Hungarian) 2025-07-10 13:17:00 +02:00
Flomp
f313cded51 New translations en.json (German) 2025-07-10 13:16:59 +02:00
Flomp
e8a356c52d New translations en.json (French) 2025-07-10 13:16:58 +02:00
Flomp
9585d9ec1f New translations en.json (Spanish) 2025-07-10 13:16:57 +02:00
Christian Beutel
86c40d8319 fixes homepage categories 2025-07-10 13:16:40 +02:00
Flomp
24448b2f32 New translations en.json (Dutch) 2025-07-05 18:44:06 +02:00
Christian Beutel
7c0685d887 fixes lists back button 2025-07-04 15:10:52 +02:00
Flomp
52302bc517 New translations en.json (Basque) 2025-07-04 14:06:17 +02:00
Flomp
da41413394 New translations en.json (Russian) 2025-07-04 14:06:16 +02:00
Flomp
f0b58e7949 New translations en.json (Chinese Simplified) 2025-07-04 14:06:15 +02:00
Flomp
b7ebff3586 New translations en.json (Portuguese) 2025-07-04 14:06:13 +02:00
Flomp
26d1e2728d New translations en.json (Polish) 2025-07-04 14:06:12 +02:00
Flomp
1cd99c4980 New translations en.json (Dutch) 2025-07-04 14:06:11 +02:00
Flomp
e0f25137c8 New translations en.json (Italian) 2025-07-04 14:06:10 +02:00
Flomp
3a9f52d055 New translations en.json (Hungarian) 2025-07-04 14:06:09 +02:00
Flomp
93082f3cdc New translations en.json (German) 2025-07-04 14:06:08 +02:00
Flomp
adb9045acb New translations en.json (French) 2025-07-04 14:06:07 +02:00
Flomp
82c95a4896 New translations en.json (Spanish) 2025-07-04 14:06:06 +02:00
Christian Beutel
ce9c737ca6 adds trail reversing 2025-07-04 13:53:55 +02:00
Christian Beutel
247f02c2fc allows removing hillshade 2025-07-04 11:57:30 +02:00
Christian Beutel
e923ec45a4 fixes valhalla URL 2025-07-04 11:21:39 +02:00
Christian Beutel
a7194027d8 allows public acces to public profiles 2025-07-04 11:11:56 +02:00
Christian Beutel
7c478f6f3f fixes list loading 2025-07-04 10:41:17 +02:00
Christian Beutel
819aea801e return json errors from valhalla api route 2025-07-04 10:16:11 +02:00
Christian Beutel
fff3bd1bf7 fixes stats summit log table 2025-07-04 10:15:54 +02:00
Christian Beutel
57234acf3a adds ogp meta tags 2025-07-04 10:00:52 +02:00
Christian Beutel
0121208f46 fixes apostrophes in waypoint description 2025-07-04 09:17:16 +02:00
Flomp
ce14d013b0 New translations en.json (Polish) 2025-07-01 11:21:26 +02:00
Flomp
3d978c87bf New translations en.json (Polish) 2025-07-01 08:41:10 +02:00
Christian Beutel
0166943720 updates server list 2025-06-30 17:17:00 +02:00
Christian Beutel
94a6915d6b updates server list 2025-06-28 13:37:16 +02:00
Christian Beutel
7f1b34e66c fixes iri in user create 2025-06-28 12:33:45 +02:00
Flomp
3d23ae64a1 New translations en.json (Basque) 2025-06-27 18:58:59 +02:00
Flomp
8bc469fbc3 New translations en.json (Russian) 2025-06-27 18:58:58 +02:00
Flomp
783bde9e07 New translations en.json (Chinese Simplified) 2025-06-27 18:58:57 +02:00
Flomp
8ad9eb5b73 New translations en.json (Portuguese) 2025-06-27 18:58:56 +02:00
Flomp
6cb91f5b48 New translations en.json (Polish) 2025-06-27 18:58:55 +02:00
Flomp
70a7e45235 New translations en.json (Dutch) 2025-06-27 18:58:54 +02:00
Flomp
a26faff6ed New translations en.json (Italian) 2025-06-27 18:58:53 +02:00
Flomp
4f941817ff New translations en.json (Hungarian) 2025-06-27 18:58:52 +02:00
Flomp
34653657f9 New translations en.json (German) 2025-06-27 18:58:51 +02:00
Flomp
8175e2566a New translations en.json (French) 2025-06-27 18:58:50 +02:00
Flomp
4af631d424 New translations en.json (Spanish) 2025-06-27 18:58:49 +02:00
Christian Beutel
efebcc176b v0.17.1 2025-06-27 18:52:57 +02:00
Christian Beutel
bc2fc68394 Merge remote-tracking branch 'origin/i10n_main' 2025-06-27 18:28:18 +02:00
Christian Beutel
67aa566888 adds server list to docs 2025-06-27 17:34:10 +02:00
Flomp
39ed56ecb4 New translations en.json (Basque) 2025-06-27 14:47:45 +02:00
Flomp
4fa058b032 New translations en.json (Russian) 2025-06-27 14:47:44 +02:00
Flomp
1ec7032f1e New translations en.json (Chinese Simplified) 2025-06-27 14:47:42 +02:00
Flomp
625b6e153f New translations en.json (Portuguese) 2025-06-27 14:47:41 +02:00
Flomp
e69f02f965 New translations en.json (Polish) 2025-06-27 14:47:40 +02:00
Flomp
5014193ac7 New translations en.json (Dutch) 2025-06-27 14:47:39 +02:00
Flomp
d1481e1d72 New translations en.json (Italian) 2025-06-27 14:47:38 +02:00
Flomp
ed9ab91fa8 New translations en.json (Hungarian) 2025-06-27 14:47:37 +02:00
Flomp
f6033e7a03 New translations en.json (German) 2025-06-27 14:47:36 +02:00
Flomp
9d3555ee84 New translations en.json (French) 2025-06-27 14:47:35 +02:00
Flomp
c60e15e228 New translations en.json (Spanish) 2025-06-27 14:47:34 +02:00
Thomas
d9c3cbe605 fix typo (#360) 2025-06-27 13:53:12 +02:00
Flomp
80911faa41 New translations en.json (French) 2025-06-27 13:21:58 +02:00
Christian Beutel
78406e3130 removes self signed certificates outside dev environments 2025-06-27 12:56:30 +02:00
Christian Beutel
753c1f1385 fixes map 2025-06-27 12:55:57 +02:00
Christian Beutel
7b57252f7f fixes mail notifications 2025-06-27 11:51:23 +02:00
Christian Beutel
277b435c65 fixes private profile issues 2025-06-26 21:19:53 +02:00
Christian Beutel
65c04781cc fix graticule 2025-06-26 19:03:42 +02:00
Flomp
9fb38abda3 New translations en.json (Basque) 2025-06-26 15:44:53 +02:00
Flomp
691418df5d New translations en.json (Russian) 2025-06-26 15:44:52 +02:00
Flomp
9c37c19188 New translations en.json (Chinese Simplified) 2025-06-26 15:44:52 +02:00
Flomp
d860481723 New translations en.json (Portuguese) 2025-06-26 15:44:51 +02:00
Flomp
d807eaaf9b New translations en.json (Polish) 2025-06-26 15:44:50 +02:00
Flomp
7ab8c843e2 New translations en.json (Dutch) 2025-06-26 15:44:48 +02:00
Flomp
2902f4f07a New translations en.json (Italian) 2025-06-26 15:44:47 +02:00
Flomp
b86e735bac New translations en.json (Hungarian) 2025-06-26 15:44:46 +02:00
Flomp
9c497ea2e4 New translations en.json (German) 2025-06-26 15:44:45 +02:00
Flomp
f2badbeb12 New translations en.json (French) 2025-06-26 15:44:44 +02:00
Flomp
99f852b74c New translations en.json (Spanish) 2025-06-26 15:44:43 +02:00
slothful-vassal
06f6d58b5e Trail Table: Allow multi select (#264)
* trail table: multi select

* trail cards: multi select

* multiselect for list view, several multiselect fixes, code beautify

* remove unnecessary imports, move code to simplify diff of trail_dropdown

* translations

* fix horizontal scrollbar in table view

* direct export from list, multiselect export

* fix refreshing share icon after sharing from trail list

* fix adding multiple trails to a trail-list

* fix retrieving mail notification template

* migrates to v0.17.0

---------

Co-authored-by: Christian Beutel <>
2025-06-26 15:28:27 +02:00
Flomp
e70d136ad9 New translations en.json (Basque) 2025-06-26 14:41:58 +02:00
Flomp
0c6fdc21bf New translations en.json (Russian) 2025-06-26 14:41:57 +02:00
Flomp
00e64ff57f New translations en.json (Chinese Simplified) 2025-06-26 14:41:56 +02:00
Flomp
c6aa1e1800 New translations en.json (Portuguese) 2025-06-26 14:41:54 +02:00
Flomp
fe9f47a228 New translations en.json (Polish) 2025-06-26 14:41:53 +02:00
Flomp
79b7ddd4f5 New translations en.json (Dutch) 2025-06-26 14:41:52 +02:00
Flomp
ccd9f64f48 New translations en.json (Italian) 2025-06-26 14:41:51 +02:00
Flomp
0e1c799ef5 New translations en.json (Hungarian) 2025-06-26 14:41:50 +02:00
Flomp
2694589d90 New translations en.json (German) 2025-06-26 14:41:49 +02:00
Flomp
63a7ec5a92 New translations en.json (French) 2025-06-26 14:41:48 +02:00
Flomp
7803d9fc1b New translations en.json (Spanish) 2025-06-26 14:41:46 +02:00
Brian Nelson
9b6522ec21 Enhance Tags on TrailCard component with toggle support (#285)
* Updated trail card to show first 2 tags and made tags smaller

* fixes ux and adds i18n

---------

Co-authored-by: Christian Beutel <>
2025-06-26 13:22:54 +02:00
Flomp
47e113efa2 New translations en.json (Basque) 2025-06-26 13:16:08 +02:00
Flomp
13e047f94e New translations en.json (Russian) 2025-06-26 13:16:07 +02:00
Flomp
eea73497fc New translations en.json (Chinese Simplified) 2025-06-26 13:16:06 +02:00
Flomp
6f0a92a474 New translations en.json (Portuguese) 2025-06-26 13:16:05 +02:00
Flomp
5160918078 New translations en.json (Polish) 2025-06-26 13:16:04 +02:00
Flomp
3961fc0c49 New translations en.json (Dutch) 2025-06-26 13:16:03 +02:00
Flomp
73544a54b5 New translations en.json (Italian) 2025-06-26 13:16:02 +02:00
Flomp
52b6c7b58c New translations en.json (Hungarian) 2025-06-26 13:16:01 +02:00
Flomp
1703fe1c23 New translations en.json (German) 2025-06-26 13:16:00 +02:00
Flomp
e11354b976 New translations en.json (French) 2025-06-26 13:15:59 +02:00
Flomp
642cc36326 New translations en.json (Spanish) 2025-06-26 13:15:58 +02:00
Flomp
a8e8343e6d New translations en.json (Dutch) 2025-06-24 14:07:30 +02:00
Flomp
797e31d1e6 New translations en.json (Russian) 2025-06-23 20:17:06 +02:00
Flomp
2f30a3a3f7 New translations en.json (Russian) 2025-06-23 18:46:03 +02:00
Flomp
fd50b06c7f New translations en.json (Basque) 2025-06-23 17:02:59 +02:00
Flomp
c85f83f56e New translations en.json (Basque) 2025-06-21 15:31:55 +02:00
Flomp
80e2222d4a New translations en.json (Russian) 2025-06-21 12:33:53 +02:00
Flomp
7768fd17bf New translations en.json (Chinese Simplified) 2025-06-21 12:33:52 +02:00
Flomp
3adadfb365 New translations en.json (Portuguese) 2025-06-21 12:33:52 +02:00
Flomp
67f542b40e New translations en.json (Polish) 2025-06-21 12:33:51 +02:00
Flomp
39eedcd848 New translations en.json (Dutch) 2025-06-21 12:33:50 +02:00
Flomp
16e6d4df83 New translations en.json (Italian) 2025-06-21 12:33:49 +02:00
Flomp
d8f3cb1abd New translations en.json (Hungarian) 2025-06-21 12:33:48 +02:00
Flomp
450b34b4d6 New translations en.json (German) 2025-06-21 12:33:47 +02:00
Flomp
79ab48596b New translations en.json (French) 2025-06-21 12:33:46 +02:00
Flomp
b4ef7d8296 New translations en.json (Spanish) 2025-06-21 12:33:45 +02:00
Flomp
3baf47d864 New translations en.json (German) 2025-06-21 11:25:36 +02:00
545 changed files with 64396 additions and 52340 deletions

24
.github/ISSUE_TEMPLATE/bug_report.yml vendored Normal file
View File

@@ -0,0 +1,24 @@
name: "🐛 Bug Report"
description: File a bug report.
labels: ["bug"]
body:
- type: markdown
attributes:
value: |
Thanks for taking the time to fill out this bug report!
- type: textarea
id: what-happened
attributes:
label: What happened?
description: Also, please tell us what you expected to happen. Please convert .gpx files to .txt and attach them when appropriate.
placeholder: Tell us what you see!
validations:
required: true
- type: input
id: version
attributes:
label: Version
description: What version of wanderer are you running?
placeholder: ex. v0.17.2
validations:
required: true

8
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View File

@@ -0,0 +1,8 @@
blank_issues_enabled: false
contact_links:
- name: "💡 Feature request"
url: https://github.com/open-wanderer/wanderer/discussions/new?category=ideas
about: Suggest an idea for this project.
- name: "❓ Ask a question"
url: https://github.com/open-wanderer/wanderer/discussions/new?category=q-a
about: Please ask and answer questions here.

83
.github/dependabot.yml vendored Normal file
View File

@@ -0,0 +1,83 @@
version: 2
updates:
- package-ecosystem: gomod
open-pull-requests-limit: 10
directory: "/db"
schedule:
interval: "monthly"
groups:
gomod-backward-compatible:
update-types:
- minor
- patch
cooldown:
default-days: 7
- package-ecosystem: github-actions
open-pull-requests-limit: 5
directory: "/"
schedule:
interval: "monthly"
groups:
github-actions-backward-compatible:
update-types:
- minor
- patch
cooldown:
default-days: 7
- package-ecosystem: npm
open-pull-requests-limit: 5
directory: "/web"
schedule:
interval: "monthly"
groups:
npm-backward-compatible:
update-types:
- minor
- patch
cooldown:
default-days: 7
- package-ecosystem: npm
open-pull-requests-limit: 5
directory: "/docs"
schedule:
interval: "monthly"
groups:
docker-backward-compatible:
update-types:
- minor
- patch
cooldown:
default-days: 7
- package-ecosystem: "docker"
directory: "/docs"
schedule:
interval: "monthly"
groups:
docker-docs-backward-compatible:
update-types:
- minor
- patch
cooldown:
default-days: 7
- package-ecosystem: "docker"
directory: "/search"
schedule:
interval: "monthly"
groups:
docker-search-backward-compatible:
update-types:
- minor
- patch
cooldown:
default-days: 7
- package-ecosystem: "docker"
directory: "/web"
schedule:
interval: "monthly"
groups:
docker-web-backward-compatible:
update-types:
- minor
- patch
cooldown:
default-days: 7

33
.github/workflows/go.yml vendored Normal file
View File

@@ -0,0 +1,33 @@
name: Go
on:
push:
branches: [ main ]
paths:
- '.github/**'
- 'db/**'
pull_request:
paths:
- '.github/**'
- 'db/**'
jobs:
db-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/setup-go@v6
with:
go-version: '1.25'
- run: make db-fmt
- name: Ensure formatting
run: |
if [ -n "$(gofmt -l .)" ]; then
echo "Go files are not formatted"
exit 1
fi
git diff --exit-code
working-directory: db
- run: make db-vet
- run: make db-test

View File

@@ -1,139 +1,97 @@
name: Release Workflow
name: Publish Release
on:
workflow_dispatch:
inputs:
version:
description: "The version to release (e.g., 0.12.0)"
required: true
pull_request:
types: [closed]
branches: [main]
jobs:
# Job 1: Bump Versions and Create Tags
versioning:
# Only run if the PR was merged AND it came from a release branch
publish:
if: github.event.pull_request.merged == true && startsWith(github.head_ref, 'release/v')
runs-on: ubuntu-latest
permissions:
contents: write
outputs:
version: ${{ steps.set_version.outputs.version }}
version: ${{ steps.get_version.outputs.version }}
steps:
# 1. Checkout the repository
- name: Checkout code
uses: actions/checkout@v3
uses: actions/checkout@v6
# 2. Setup node & npm
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '22'
# 3. Bump versions using npm
- name: Bump version in web and docs
id: set_version
run: |
VERSION=${{ github.event.inputs.version }}
cd web && npm version $VERSION --no-git-tag-version && cd ..
cd docs && npm version $VERSION --no-git-tag-version && cd ..
echo "version=v$VERSION" >> $GITHUB_OUTPUT
# 4. Tag and push the new versions
- name: Commit and Tag
- name: Set Version Output
id: get_version
run: |
# Extract v0.12.0 from release/v0.12.0
VERSION_TAG=${GITHUB_HEAD_REF#release/}
echo "version=$VERSION_TAG" >> $GITHUB_OUTPUT
# Git Tagging
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add web/package.json docs/package.json
git commit -m "Release ${{ github.event.inputs.version }}"
git tag "v${{ github.event.inputs.version }}"
git push origin main --tags
git tag $VERSION_TAG
git push origin $VERSION_TAG
# Job 2: Build Docker Images
docker-build:
needs: publish
runs-on: ubuntu-latest
needs: versioning
steps:
# 1. Checkout the repository
- name: Checkout code
uses: actions/checkout@v3
with:
ref: ${{ github.ref }}
- uses: actions/checkout@v6
- name: Setup Go
uses: actions/setup-go@v4
uses: actions/setup-go@v6
with:
go-version: '1.22'
# 2. Log in to Docker Hub
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: '22'
- name: Login to Docker Hub
uses: docker/login-action@v3
uses: docker/login-action@v4
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
# 3. Setup docker multi platform builds
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
uses: docker/setup-buildx-action@v4
# 4. Build and push Docker images
- name: Build Docker Images
env:
VERSION: ${{ needs.versioning.outputs.version }}
VERSION: ${{ needs.publish.outputs.version }}
run: |
# Build db image
# cd db
# env GOOS=linux GOARCH=arm64 go build -o pocketbase_arm64
# env GOOS=linux GOARCH=amd64 go build -o pocketbase_amd64
# cd ..
# docker buildx build db/ --no-cache -t flomp/wanderer-db:$VERSION -t flomp/wanderer-db:latest --platform=linux/amd64,linux/arm64 --push
# Build web image
export PUBLIC_VALHALLA_URL=https://valhalla.openstreetmap.de
cd web
npm ci && npm run build
cd ..
docker buildx build db/ --no-cache -t flomp/wanderer-db:$VERSION -t flomp/wanderer-db:latest --platform=linux/amd64,linux/arm64 --push
npm --prefix web ci
npm --prefix web run openapi:generate
docker buildx build web/ --no-cache -t flomp/wanderer-web:$VERSION -t flomp/wanderer-web:latest --platform=linux/amd64,linux/arm64 --push
npm --prefix docs ci
npm --prefix docs run build
docker buildx build ./docs --no-cache -t flomp/wanderer-docs:$VERSION -t flomp/wanderer-docs:latest --platform=linux/amd64,linux/arm64 --push
# Build docs image
cd docs
npm ci && npm run build
cd ..
docker buildx build docs/ --no-cache -t flomp/wanderer-docs:$VERSION -t flomp/wanderer-docs:latest --platform=linux/amd64,linux/arm64 --push
# Job 3: Publish the Release
release:
needs: [publish, docker-build]
runs-on: ubuntu-latest
needs: [versioning, docker-build]
permissions:
contents: write
steps:
# 1. Checkout the repository
- name: Checkout code
uses: actions/checkout@v3
with:
ref: ${{ github.ref }}
# 2. Extract release notes from CHANGELOG.md
- uses: actions/checkout@v6
- name: Extract release notes
id: changelog
run: |
VERSION="${{ needs.versioning.outputs.version }}"
CHANGELOG=$(awk -v ver="$VERSION" '
BEGIN { in_section = 0 }
/^# / {
if (in_section) exit
if ($2 == ver) in_section = 1
}
in_section { print }
' CHANGELOG.md)
VERSION="${{ needs.publish.outputs.version }}"
# Clean 'v' from v0.12.0 for awk if your changelog uses 0.12.0
RAW_VER=${VERSION#v}
CHANGELOG=$(awk -v ver="$RAW_VER" 'BEGIN {in_section=0} /^# / {if (in_section) exit; if ($2 == ver) in_section=1} in_section {print}' CHANGELOG.md)
echo 'changelog<<EOF' >> $GITHUB_OUTPUT
printf "%s\n" "$CHANGELOG" >> $GITHUB_OUTPUT
printf "$CHANGELOG" >> $GITHUB_OUTPUT
echo 'EOF' >> $GITHUB_OUTPUT
# 3. Create GitHub Release
- name: Create GitHub Release
uses: actions/create-release@v1
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ needs.versioning.outputs.version }}
release_name: "${{ needs.versioning.outputs.version }}"
tag_name: ${{ needs.publish.outputs.version }}
body: ${{ steps.changelog.outputs.changelog }}
draft: false
prerelease: false
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

46
.github/workflows/release_dev.yaml vendored Normal file
View File

@@ -0,0 +1,46 @@
name: Publish dev Release
on:
push:
branches:
- main
jobs:
docker-build:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: '22'
- name: Bump versions
run: |
LAST_TAG=$(git describe --tags --abbrev=0)
BASE_VERSION=${LAST_TAG#v}
FULL_SHA=$(git rev-parse HEAD)
VERSION="${BASE_VERSION}-dev+${FULL_SHA}"
cd web && npm version $VERSION --no-git-tag-version && cd ..
cd docs && npm version $VERSION --no-git-tag-version && cd ..
- name: Login to GHCR
uses: docker/login-action@v4
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4
- name: Build Docker Images
run: |
docker buildx build db/ --no-cache -t ghcr.io/open-wanderer/wanderer-db:dev --push
docker buildx build web/ --no-cache -t ghcr.io/open-wanderer/wanderer-web:dev --push

39
.github/workflows/release_request.yaml vendored Normal file
View File

@@ -0,0 +1,39 @@
name: Release Request
on:
workflow_dispatch:
inputs:
version:
description: "The version to release (e.g., 0.12.0)"
required: true
jobs:
create-pr:
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
steps:
- uses: actions/checkout@v6
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: '22'
- name: Bump versions
run: |
VERSION=${{ github.event.inputs.version }}
cd web && npm version $VERSION --no-git-tag-version && cd ..
cd docs && npm version $VERSION --no-git-tag-version && cd ..
- name: Create Pull Request
uses: peter-evans/create-pull-request@v8
with:
token: ${{ secrets.GITHUB_TOKEN }}
commit-message: "chore: release v${{ github.event.inputs.version }}"
branch: "release/v${{ github.event.inputs.version }}"
title: "Release v${{ github.event.inputs.version }}"
body: "This PR bumps the version. Merging this will trigger Docker builds and a GitHub Release."
base: main

38
.github/workflows/web.yaml vendored Normal file
View File

@@ -0,0 +1,38 @@
name: Web CI
on:
push:
branches: [ main ]
paths:
- '.github/**'
- 'web/**'
pull_request:
paths:
- '.github/**'
- 'web/**'
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
with:
node-version: '22'
- uses: actions/setup-go@v6
with:
go-version: '1.25'
- run: make db-build-docker
- run: make web-build-docker
- run: docker compose up -d
- run: make web-install
- run: make web-playwright-install
- run: make web-check
- run: make web-test

2
.gitignore vendored
View File

@@ -9,6 +9,6 @@ search/dumps
run.sh
build*.sh
start.*
start*.*
data*/

View File

@@ -1,10 +1,227 @@
# v0.19.1
## Features
- Speed improvements for various database queries
## Security
- Remote lists, remote trails, and remote trail comments now respect the visibility settings and shares of the respective list or trail. (PR #980)
## Bug Fixes
- Federated remote trails now sync tags and create missing local tags when needed. (PR #987)
- Private profiles no longer prevent access to a user's own trails; inaccessible private profiles now return a proper `404`. (PR #986)
- Comment access rules now correctly check the linked comment author via `author.user`. (PR #984)
# v0.19.0
## Breaking Changes
- Bulk uploads no longer use `UPLOAD_USER` / `UPLOAD_PASSWORD` authentication. Uploads now require an API token; files must be placed in a subfolder of the upload directory named after the respective API token. For more information checkout the [documentation](https://wanderer.to/use/import-export/) (PR #886).
- Bulk uploads now run via a file watcher rather than on a cron schedule. Files placed in the upload folder while the container is not running will not be processed automatically. (PR #886)
- External service URLs have been moved from public frontend variables to server-side variables: `VALHALLA_URL`, `NOMINATIM_URL`, `OVERPASS_API_URL`. The old `PUBLIC_*` variables remain as a fallback but should be migrated. (PR #697)
- The waypoint data model has been extended for federation: waypoints now have an `iri` and reference their author via an ActivityPub actor. This only affects clients that access PocketBase collections directly, not the standard UI. (PR #930)
- ActivityPub actor counters have been renamed to `follower_count` and `following_count`. This only affects clients that access PocketBase collections directly. (PR #930)
- OpenAPI documentation is now generated from annotations and served as JSON (the YAML endpoint has been removed). (PR #927)
## Security
- HTML content in descriptions, comments, summit logs, waypoints, and profile bios is now sanitised on the server to reduce the risk of cross-site scripting (XSS). Some custom HTML may be stripped on save. (PR #930)
- Anonymous user API endpoints have been removed. This only affects third-party applications that accessed user data without authentication. Regular users and the standard UI are unaffected. (PR #927)
- Additional CSRF/SSRF protections and rate limiting have been implemented for ActivityPub and outbound network calls. (PR #930)
## Features
- Hammerhead integration added, including synchronisation of planned and completed tours, and manual trail sending. (PR #628)
- The federation has been significantly expanded and refactored to provide more robust remote content synchronisation and local caching for remote trails and lists. (PR #930)
- Trails can now be explicitly marked as completed. (PR #920)
- Geotagged waypoint photos are now automatically merged into existing or nearby waypoints within the configured merge radius of the category. (PR #457)
- The trail overview now has narrow and wide view modes, as well as improved multi-select. (PR #666, #921)
- Trails can be copied directly and their visibility can be changed more easily. (PR #571)
- External geocoding, routing, and Overpass calls now run on the server. (PR #697)
- New setting: optionally start drawing a new trail from the current location. (PR #592)
- GPX exports now include additional metadata for waypoints. (PR #919)
- FIT import now uses a more compatible parser. (PR #884)
- Frontpage performance has been improved. (PR #929)
- 3D terrain rendering has been improved. (PR #881)
- Improved saving of public lists. (PR #554)
- Multiple trails can now be merged into one trail with multiple summit logs. This feature is also available as an automatic option when importing trails via an integration (PR #627)
- API tokens have been added so that external tools and automations can interact with Wanderer. (PR #848)
## Bug Fixes
- Fixed broken WebFinger requests. (PR #966)
- Theme detection fixed via corrected `color-scheme` query selector. (PR #957, thanks @mfortini)
- Hillshading visibility on the map has been fixed. (PR #942)
- Komoot sync now runs to completion. (PR #917, thanks @StefanSchloegl)
- Komoot integration now handles invalid photos more robustly. (PR #941)
- Mentions now work correctly when `username` and `preferred_username` differ. (PR #885)
- Fixed avatar updates. (PR #870)
- List descriptions in the selection modal are now fixed. (PR #869)
- Fixed double GPX upload when creating a trail. (PR #969)
- Fixed authentication issues after email change (PR #973)
- Fixed help links. (PR #938)
- Fixed a MapLibre layer manager issue that could prevent existing map layers from being tracked correctly after data updates. (PR #960, thanks @palhaland)
- Fixed search endpoints returning invalid errors in some failure cases. (PR #961, thanks @palhaland)
## Translation
- The Norwegian translations have been updated. (PR #931, thanks @palhaland)
## Maintenance
- Meilisearch, PocketBase, Go, web/docs dependencies, CI actions, and Docker build setup updated.
# v0.18.5
## Security
- Fixes CVE-2022-39299 via xmldom upgrade (PR #820)
## Features
- Persist trail list filter settings in local storage: filters are preserved on reload and when navigating back from a trail, and reset when - leaving the trail list (PR #814)
## Bug Fixes
- Skip elevation correction when Valhalla returns invalid (null) elevation data; original GPX values are preserved (PR #821)
- Improved threshold handling for high-frequency GPX tracks to ensure correct ascent/descent calculation (PR #813)
- Fixes trail upload for new users without default privacy settings (PR #785)
- Fixes focus loss and duplicate “Cancel” button in trail filter search (Chrome) (PR #738)
- Fixes async state issue in list search modal (bulk assignment works again) (PR #816)
- Fixes locale errors and improved dynamic locale detection (PR #656)
- Fixes PDF/print layout so descriptions render correctly and no longer overlap waypoints(Thanks to @RobertClarke64) (PR #797)
- Corrected POCKETBASE_SMTP_SENDER_ADDRESS spelling (previously POCKETBASE_SMTP_SENDER_ADRESS, now deprecated but backwards compatible) (PR #769)
- Fixes amenity naming in stored map state, including migration (PR #768)
- Fixes race condition in map plugin that caused errors when printing a trail (PR #827)
## Translation
- Added Czech language support (thanks @Sheepa) (PR #774)
## Dependencies
- Updated most dependencies, including security and maintenance updates
# v0.18.4
## Bug fixes
- Tags can now properly be removed from trails
- Creating more than 29 lists does no longer prevent lists from loading
- Fixes a bug that caused the GPS data to be removed from summit logs after editing
- Fixes an issue that caused an infinite loop when creating a list with federation being active
- Fixes an issue where bicycle routing options were incorrectly applied to car routing, and vice versa
- Fixes an issue where filtering by multiple categories did not work as expected
- Fixes imported tracks being marked private despite public-by-default settings
## Features
- Trails can now be added to multiple lists at once
- ActivityPub: External user access now requires authentication (401)
## Translation
- Adds Norwegian translation (thanks @palhaland)
# v0.18.3
## Bug fixes
- Fixes bug that prevented waypoints from being update or deleted in v0.18.2
- Return properly formatted error message when accessing a protected API route
- Fixes logo for OIDC 2 & 3 providers (thanks @wolffshots)
- Fixes bug that caused dropdown actions from a feed item on the homepage to cause a 404 error
## Maintenance
- Updates pocketbase to v0.30.0
# v0.18.2
## Features
- Adds `dedup` command to pocketbase. This command allows an admin to quickly identify duplicate trails and delete them. Use the `--dry-run` flag to only log duplicate trails without deleting them. To execute the command run `docker exec -it wanderer-db ./pocketbase dedup --dry-run`.
- Adds option to only sync strava activities after a certain date
- Singificant performance improvements for instances with larger userbases
- Greatly improved initial indexing speed when starting wanderer
## Bug fixes
- Fixes permission issues for public trails
- Fixes bug that caused trails to be duplicated multiple times (to clean up see the `dedup` command above)
- Fixes link to "New Trail" from empty profiles
- Fixes link when opening a trail from the map searchbar
- Sorting by difficulty no longer sorts by difficulty alphabetically
- Fixes strava integration stopping after only one page
# v0.18.1
## Bug fixes
- Fixes permission issues that prevented federation from working properly
- Trail categories are properly displayed in lists
- Fixes issue that prevented lists from saving
## Documentation
- Improves docs for updating on bare-metal installations
## Translation
- Adds Basque and Russian translations (thanks @aisaivia & @jeffscrum)
# v0.18.0
## Features
- Adds feed on homepage for logged in users
- Adds customizable "About" section to the homepage (read [here](https://wanderer.to/run/frontend-configuration/about) to learn more)
- New Maps: OpenHikingMap, CyclOSM
- New map overlays: hiking, biking, mountain biking & skiing routes
- New POI overlays: e.g. grocery stores, campsites, water sources etc.
- Complete overhaul of the route drawing/editing feature
- Adds option to crop routes when editing
- Adds undo/redo to route editing
- Adds option to manually recalculate elevation data when editing a route
- Adds automated elevation correction when uploading trails/summit logs directly
- Directions to a trail head are now provided by OpenStreetMap
- Non-pulbic trails can now be shared with guests via a public share link
- Adds route previews on lower zoom levels in the world map
> [!NOTE]
To display the previews for each track on the world map, wanderer computes encoded polylines for each track on startup. Depending on the amount of trails in your instance, it can take several minutes until all trails are fully indexed and searchable.
- 3D terrain is now also available in the world map
- Trail category is now displayed in search results
## Bug fixes
- Custom categories with spaces are now handled correctly
- Avoid reuploading all trails when updating a list
- Fixes build dependencies for building from source (thanks @slothful-vassal)
- Add headers to nominatim requests to comply with usage policy
- Reset pagination when updating filters
## Thanks
A big thanks goes to @cugu for doing a lot of GitHub house keeping and submitting various, helpful PRs while I was gone!
Another big shoutout has to go to @vcoppe and [gpx.studio](https://github.com/gpxstudio/gpx.studio). Their code was a huge help implementing the various map overlays and GPX editing functionality that was added in this patch.
# v0.17.2
## Features
- Trails in the map view can now be sorted
- Adds ogp metadata tags for SEO
- Public profiles are now accessible by anonymous users
- A trail's direction can now be reversed in the editor
- Adds localization for the calendar component (thanks @james-geiger)
## Bug fixes
- Waypoint descriptions are now properly formatted
- The summit log table on the statistics page shows data again
- Fixes bug that caused lists to disappear when having more than 5 lists
- Removing the hillshading URL is now possible
- Clicking on a category on the homepage links to the correct trails page again
- Fixes bug that caused trail to disappear when switching map style to OpenTopoMap
- Trails are now correctly marked as "(Not) Completed" when adding (deleting) a summit log
- Allow links to other sites when hosting a private instance
# v0.17.1
## Features
- Adds batch actions for trails. You can now select multiple trails from the list and add them to a list, for example. Big thanks to @slothful-vassal for the PR
- If a trail has more than two tags they are now toggleable on trail cards for less visual clutter. Thank to @briannelson95 for the PR
- The wanderer.to homepage now contains a dedicated "Servers" section where public instances are listed
## Bug fixes
- Searching for a trail on the homepage does no longer result in 404
- An actor's username and preferred username are no longer switched
- Accesing your own private profile no longer throws an error
- Reverse geocoding location lookup now also properly works when uploading a trail through the API
- Both trail's and summit log's duration is now stored in seconds for consistency
- All federated requests are now properly signed
- Fixes GPX parser to deal with empty tags
- Fixes bug that caused only 30 entries to be displayed in the statistics
- The "Add to list" button is available again when creating a trail
- Fixes bug that prevented saving lists with a large number of trails
- Fixes "Copy link" button when sharing lists
- Adds missing namespace to activitypub actor endpoint
# v0.17.0
> [!CAUTION]
This release contains breaking changes. They are marked with a ⚠️.
**Please update to version v0.16.5 first before updating to v0.17.0.**
## Configuration
Check the reopsitory's [`docker-compose.yml`](https://github.com/Flomp/wanderer/blob/main/docker-compose.yml) for a valid configuration.
Check the reopsitory's [`docker-compose.yml`](https://github.com/open-wanderer/wanderer/blob/main/docker-compose.yml) for a valid configuration.
- ⚠️ The PocketBase environment variable `POCKETBASE_ENCRYPTION_KEY` is now required. It requires a valid 32 character AES key as its value. To generate a key, run `openssl rand -hex 16`.
- ⚠️ The PocketBase environment variable `ORIGIN`is now required. It must be set to the public IP or hostname (including the port) of your wanderer frontend and must equal the value set for the frontend's `ORIGIN` environment variable.
@@ -118,7 +335,7 @@ This release contains breaking changes. The necessary migrations will happen aut
- Waypoints can now be created by clicking on the map when creating a new trail
- Adds support for videos
## Bugfixes
## Bug fixes
- Fixes map trail bounding box to include public and shared trails
- Fixes bug that caused orphan waypoints and summit logs
- The default language is now set correctly after registering
@@ -253,7 +470,7 @@ This release contains breaking changes. Most migrations will happen automaticall
## Maintenance
- Updates to meilisearch version 0.11.3.
- meilisearch indices are not compatible across minor versions. This means you will need to rename or delete your [`data.ms`](https://github.com/Flomp/wanderer/blob/8635de78b9f1510e2316b08e605b175a2615f4db/docker-compose.yml#L19) folder on your host system to force meilisearch to rebuild the index on the next start (note that this can take a little while).
- meilisearch indices are not compatible across minor versions. This means you will need to rename or delete your [`data.ms`](https://github.com/open-wanderer/wanderer/blob/8635de78b9f1510e2316b08e605b175a2615f4db/docker-compose.yml#L19) folder on your host system to force meilisearch to rebuild the index on the next start (note that this can take a little while).
## Features
- Adds password reset email function for users (see [docs](https://wanderer.to/guides/authentication/#forgot-your-password) for more info)
@@ -327,7 +544,7 @@ As the number of contributors to this project continues to grow (which Im ver
- The current page is now remembered when navigating back to the trail overview
## Bug fixes
- Fixed map height when viewing a trail in detail view
- Fixes map height when viewing a trail in detail view
# v0.8.1
## Features
@@ -404,7 +621,7 @@ As the number of contributors to this project continues to grow (which Im ver
- Adds missing translations
## Bug fixes
- Fixed a bug that prevented comments from showing up
- Fixes a bug that prevented comments from showing up
- GPX files without a name in the metadata section will now receive a generic name when uploaded through the API
# v0.5.1
@@ -412,7 +629,7 @@ As the number of contributors to this project continues to grow (which Im ver
- You can now export trails as GPX or GEOJson files. Optionally you can include photos and summit book entries of the trail. This replaces the "Download GPX" function in previous versions.
## Bug fixes
- Fixed a bug that would prevent users from creating multiple summit log entries without reloading the page
- Fixes a bug that would prevent users from creating multiple summit log entries without reloading the page
- wanderer now takes the `<rte>` tag into account when displaying a trail on the map
- A trail's date attribute is now the current date by default
@@ -424,7 +641,7 @@ As the number of contributors to this project continues to grow (which Im ver
## Features
- Trails can now be filtered by date
- Elevation, slope and speed graphs are now also visible when creating a new trail
- When creating a new trail you now have the option to create a new route from scratch without uploading a GPX file. Press the "Draw a route" button and plan your new route directly in wanderer. We use [valhalla](https://github.com/valhalla/valhalla) and their associated free [hosted service](https://gis-ops.com/global-open-valhalla-server-online/) to calculate the routes. To activate the feature make sure to set the PUBLIC_VALHALLA_URL environment variable on you wanderer-web service. See the current [docker-compose.yml](https://github.com/Flomp/wanderer/blob/main/docker-compose.yml) for a working configuration.
- When creating a new trail you now have the option to create a new route from scratch without uploading a GPX file. Press the "Draw a route" button and plan your new route directly in wanderer. We use [valhalla](https://github.com/valhalla/valhalla) and their associated free [hosted service](https://gis-ops.com/global-open-valhalla-server-online/) to calculate the routes. To activate the feature make sure to set the PUBLIC_VALHALLA_URL environment variable on you wanderer-web service. See the current [docker-compose.yml](https://github.com/open-wanderer/wanderer/blob/main/docker-compose.yml) for a working configuration.
## Bug fixes
- Uploaded trails will now have a date if it can be parsed from the file
@@ -438,13 +655,13 @@ As the number of contributors to this project continues to grow (which Im ver
- Adds a setting to focus the map on all trails instead of a specific location
## Bug fixes
- fixed a bug that would show a wrong date for summit logs for certain time zones (now really)
- fixed a bug that prevented waypoints from showing up in public trails
- Fixes a bug that would show a wrong date for summit logs for certain time zones (now really)
- Fixes a bug that prevented waypoints from showing up in public trails
# v0.3.2
## Bug fixes
- Fixed a bug that caused a 500 Internal Error to appear when viewing trails without an account
- Fixes a bug that caused a 500 Internal Error to appear when viewing trails without an account
## Translations
@@ -459,7 +676,7 @@ As the number of contributors to this project continues to grow (which Im ver
## Bug fixes
- Fixed a bug that prevented import trails from appearing in map view
- Fixes a bug that prevented import trails from appearing in map view
## Translations
@@ -473,13 +690,13 @@ As the number of contributors to this project continues to grow (which Im ver
## Features
- Trails can now be added to a list while editing or creating a trail. The trail must be saved at least once to add it to a list.
- wanderer now has an auto-upload folder. GPX files in this folder will be autmatically uploaded and converted to a trail. Read the [docs](https://github.com/Flomp/wanderer/wiki/API#auto-upload-folder) for more information.
- wanderer now has an auto-upload folder. GPX files in this folder will be autmatically uploaded and converted to a trail. Read the [docs](https://github.com/open-wanderer/wanderer/wiki/API#auto-upload-folder) for more information.
- addded support for TCX and KML files. Note that this feature is still experimental. Please report any issues you encounter.
- added OAuth support. Read [here](https://github.com/Flomp/wanderer/wiki/OAuth) how to enable providers.
- added OAuth support. Read [here](https://github.com/open-wanderer/wanderer/wiki/OAuth) how to enable providers.
## Bug fixes
- fixed a bug that would show a wrong date for summit logs for certain time zones
- Fixes a bug that would show a wrong date for summit logs for certain time zones
- added client side validation for usernames
## Translations
@@ -491,7 +708,7 @@ As the number of contributors to this project continues to grow (which Im ver
## Bug fixes
- summit book dates now show in the correct format for the current locale
- fixed a bug that would overwrite trail names and descriptions when editing a trail
- Fixes a bug that would overwrite trail names and descriptions when editing a trail
## Translations
@@ -508,7 +725,7 @@ As the number of contributors to this project continues to grow (which Im ver
- waypoint markers can now be moved with drag & drop
- lists can now be displayed as a map showing all trails contained in the list
- you can now prevent users from signing up by setting the `DISABLE_SIGNUP` environment variable to `true`
- you can now upload GPX files via the API to create trails. Check the [documentation](https://github.com/Flomp/wanderer/wiki/API#upload-trails) for more info.
- you can now upload GPX files via the API to create trails. Check the [documentation](https://github.com/open-wanderer/wanderer/wiki/API#upload-trails) for more info.
- the city index now includes states
> Note: for city states to show up in your search you have to delete your data.ms folder if you already have a previous installation of wanderer. The indices will then be rebuilt on startup.
@@ -520,10 +737,10 @@ As the number of contributors to this project continues to grow (which Im ver
# v0.1.1
## Bug fixes
- fixed a bug that would prevent trails longer than 20km from being displayed
- Fixes a bug that would prevent trails longer than 20km from being displayed
- added BODY_SIZE_LIMIT env variable to docker compose to allow for bigger file uploads
- fixed a bug that caused only 5 trails to be shown at a time
- fixed a bug that would cause waypoints not to be deleted from the backend
- Fixes a bug that caused only 5 trails to be shown at a time
- Fixes a bug that would cause waypoints not to be deleted from the backend
- updated the default docker-compose.yml to include a secure MEILI_MASTER_KEY
- the default location field now sets the value correctly after clicking on a search result
@@ -532,4 +749,4 @@ As the number of contributors to this project continues to grow (which Im ver
- updated the docs to include BODY_SIZE_LIMIT
# v0.1.0
- Initial release
- Initial release

43
Makefile Normal file
View File

@@ -0,0 +1,43 @@
## db
.PHONY: db-fmt
db-fmt:
cd db && go fmt ./...
.PHONY: db-vet
db-vet:
cd db && go vet ./...
.PHONY: db-test
db-test:
cd db && go test ./...
.PHONY: db-build
db-build:
cd db && CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o pocketbase_amd64
.PHONY: db-build-docker
db-build-docker: db-build
docker buildx build db/ --no-cache -t flomp/wanderer-db:latest
## Web
.PHONY: web-install
web-install:
cd web && npm install
.PHONY: web-playwright-install
web-playwright-install:
cd web && npx playwright install --with-deps chromium
.PHONY: web-check
web-check:
cd web && npm run check
.PHONY: web-test
web-test:
cd web && npm run test
.PHONY: web-build-docker
web-build-docker:
docker buildx build web/ --no-cache -t flomp/wanderer-web:latest

View File

@@ -4,8 +4,8 @@
<h4>The trail catalogue that makes your GPS data searchable</h4>
[![Docker Image Version (tag latest semver)](https://img.shields.io/docker/v/flomp/wanderer-web/latest)](https://github.com/Flomp/wanderer/)
[![GitHub Repo stars](https://img.shields.io/github/stars/flomp/wanderer?style=social)](https://github.com/Flomp/wanderer/)
[![Docker Image Version (tag latest semver)](https://img.shields.io/docker/v/flomp/wanderer-web/latest)](https://github.com/open-wanderer/wanderer/)
[![GitHub Repo stars](https://img.shields.io/github/stars/open-wanderer/wanderer?style=social)](https://github.com/open-wanderer/wanderer/)
[![Buy Me A Coffee](https://img.shields.io/badge/Support-wanderer-yellow?logo=buy-me-a-coffee)](https://www.buymeacoffee.com/wanderertrails)
[![Discord](https://img.shields.io/discord/1249895457396621332?style=social&logo=discord&label=Developer%20Discord)](https://discord.gg/USSEBY98CP)
@@ -32,7 +32,7 @@ The recommended and quickest way to install wanderer is using docker compose:
``` bash
# download the docker compose file
wget https://raw.githubusercontent.com/Flomp/wanderer/main/docker-compose.yml
wget https://raw.githubusercontent.com/open-wanderer/wanderer/main/docker-compose.yml
# build and launch via docker compose
docker compose up -d
@@ -44,7 +44,7 @@ The first startup can take up to 90 seconds after which you can access the front
> ⚠️ if you are using wanderer in a production environment make sure to change the MEILI_MASTER_KEY variable.
You can also run wanderer on bare-metal. Check out the [documentation](https://wanderer.to/run/installation/#installation-from-source) for a detailed how-to guide.
You can also run wanderer on bare-metal. Check out the [documentation](https://wanderer.to/run/installation/from-source) for a detailed how-to guide.
## Support wanderer

12
db/.dockerignore Normal file
View File

@@ -0,0 +1,12 @@
*
!commands
!federation
!hooks
!go.*
!integrations
!main.go
!migrations
!routes
!templates
!services
!util

View File

@@ -1,18 +1,56 @@
FROM alpine:3.16
FROM curlimages/curl:8.18.0 AS download-env
# renovate: datasource=github-releases depName=stunnel/static-curl packageName=stunnel/static-curl
ENV CURL_VERSION=8.18.0
RUN set -eux ; \
ARCHITECTURE="$(uname -m)" ; \
case $ARCHITECTURE in \
x86_64) ARCHITECTURE="x86_64" ;; \
aarch64 | armv8* | arm64) ARCHITECTURE="aarch64" ;; \
*) \
echo "(!) Architecture $ARCHITECTURE unsupported" ; \
exit 1 \
;; \
esac ; \
curl \
--connect-timeout 10 \
--fail \
--location \
--max-time 300 \
--output /tmp/curl.tar.xz \
--proto '=https' \
--show-error \
--silent \
--tlsv1.2 \
"https://github.com/stunnel/static-curl/releases/download/${CURL_VERSION}/curl-linux-${ARCHITECTURE}-glibc-${CURL_VERSION}.tar.xz" \
; \
tar -xJf /tmp/curl.tar.xz -C /tmp ; \
chmod +x /tmp/curl ;
FROM golang:1.25.0@sha256:5502b0e56fca23feba76dbc5387ba59c593c02ccc2f0f7355871ea9a0852cebe AS build
WORKDIR /app
COPY . .
RUN go mod download
RUN CGO_ENABLED=0 go build -ldflags="-s -w" -o /app/pocketbase
FROM scratch
WORKDIR /
COPY --from=build /app/pocketbase /pocketbase
COPY --from=build /etc/ssl/certs /etc/ssl/certs
COPY --from=download-env /tmp/curl /curl
COPY migrations ./migrations
COPY templates ./templates
ARG TARGETARCH
RUN echo ${TARGETARCH}
COPY ./pocketbase_${TARGETARCH} /pocketbase
RUN chmod +x /pocketbase
ENV MEILI_URL=http://localhost:7700
ENV MEILI_MASTER_KEY=
ENV MEILI_URL=http://localhost:7700 \
MEILI_MASTER_KEY= \
POCKETBASE_ENCRYPTION_KEY=
EXPOSE 8090
ENTRYPOINT ["/pocketbase", "serve", "--http=0.0.0.0:8090", "--dir=/pb_data"]
ENTRYPOINT ["/pocketbase", "serve", "--http=0.0.0.0:8090", "--dir=/pb_data"]

121
db/commands/dedup.go Normal file
View File

@@ -0,0 +1,121 @@
package commands
import (
"crypto/sha1"
"fmt"
"log"
"sort"
"github.com/pocketbase/pocketbase"
"github.com/pocketbase/pocketbase/core"
"github.com/spf13/cobra"
)
func Dedup(app *pocketbase.PocketBase) *cobra.Command {
var dryRun bool
cmd := &cobra.Command{
Use: "dedup",
Short: "Deduplicate trails by all matching fields",
Run: func(cmd *cobra.Command, args []string) {
records, err := app.FindAllRecords("trails")
if err != nil {
log.Fatalf("failed to fetch trails: %v", err)
}
// group by composite key
trailsByKey := make(map[string][]*core.Record)
for _, r := range records {
key := makeKey(r)
trailsByKey[key] = append(trailsByKey[key], r)
}
var duplicates []*core.Record
for _, recs := range trailsByKey {
if len(recs) <= 1 {
continue
}
// sort by created date ascending
sort.Slice(recs, func(i, j int) bool {
return recs[i].GetDateTime("created").Time().Before(recs[j].GetDateTime("created").Time())
})
original := recs[0]
dupes := recs[1:]
// print header row for original
// print original as header
fmt.Printf("\nOriginal: id=%s, name=%s, distance=%.2f, elevation_gain=%.2f, elevation_loss=%.2f, lat=%.5f, lon=%.5f, duration=%.2f, location=%s, category=%s, author=%s, created=%s\n",
original.Id,
original.GetString("name"),
original.GetFloat("distance"),
original.GetFloat("elevation_gain"),
original.GetFloat("elevation_loss"),
original.GetFloat("lat"),
original.GetFloat("lon"),
original.GetFloat("duration"),
original.GetString("location"),
original.GetString("category"),
original.GetString("author"),
original.GetDateTime("created"),
)
// print duplicates indented
for _, d := range dupes {
fmt.Printf(" Duplicate: id=%s, name=%s, distance=%.2f, elevation_gain=%.2f, elevation_loss=%.2f, lat=%.5f, lon=%.5f, duration=%.2f, location=%s, category=%s, author=%s, created=%s\n",
d.Id,
d.GetString("name"),
d.GetFloat("distance"),
d.GetFloat("elevation_gain"),
d.GetFloat("elevation_loss"),
d.GetFloat("lat"),
d.GetFloat("lon"),
d.GetFloat("duration"),
d.GetString("location"),
d.GetString("category"),
d.GetString("author"),
d.GetDateTime("created"),
)
duplicates = append(duplicates, d)
}
}
if dryRun {
fmt.Printf("\n[Dry Run] Found %d duplicates (no deletions performed)\n", len(duplicates))
return
}
// delete duplicates
for _, d := range duplicates {
if err := app.Delete(d); err != nil {
fmt.Printf("Failed to delete duplicate %s: %v\n", d.Id, err)
} else {
fmt.Printf("Deleted duplicate %s\n", d.Id)
}
}
},
}
cmd.Flags().BoolVar(&dryRun, "dry-run", false, "Show duplicates without deleting them")
return cmd
}
// makeKey creates a composite key string for duplicate detection
func makeKey(r *core.Record) string {
data := fmt.Sprintf("%s|%f|%f|%f|%f|%f|%f|%s|%s|%s",
r.GetString("name"),
r.GetFloat("distance"),
r.GetFloat("elevation_gain"),
r.GetFloat("elevation_loss"),
r.GetFloat("lat"),
r.GetFloat("lon"),
r.GetFloat("duration"),
r.GetString("location"),
r.GetString("category"),
r.GetString("author"),
)
h := sha1.Sum([]byte(data))
return fmt.Sprintf("%x", h)
}

View File

@@ -4,12 +4,9 @@ import (
"bytes"
"context"
"crypto/x509"
"database/sql"
"encoding/pem"
"fmt"
"io"
"net/http"
"net/url"
"os"
"slices"
"strings"
@@ -27,196 +24,103 @@ import (
)
func PostActivity(app core.App, actor *core.Record, activity *pub.Activity, recipients []string) error {
encryptionKey := os.Getenv("POCKETBASE_ENCRYPTION_KEY")
if len(encryptionKey) == 0 {
return fmt.Errorf("POCKETBASE_ENCRYPTION_KEY not set")
}
origin := os.Getenv("ORIGIN")
if origin == "" {
return fmt.Errorf("ORIGIN not set")
}
algs := []httpsig.Algorithm{httpsig.RSA_SHA256}
postHeaders := []string{"(request-target)", "Date", "Digest", "Content-Type", "Host"}
expiresIn := 60
body, err := jsonld.WithContext(
jsonld.IRI(pub.ActivityBaseURI),
jsonld.IRI(pub.SecurityContextURI),
).Marshal(activity)
if err != nil {
return err
}
decryptedPrivateKey, err := security.Decrypt(actor.GetString("private_key"), encryptionKey)
if err != nil {
return err
}
privateKey, err := x509.ParsePKCS1PrivateKey(decryptedPrivateKey)
if err != nil {
return err
}
pubID := actor.GetString("iri") + "#main-key"
client := &http.Client{}
var wg sync.WaitGroup
sem := semaphore.NewWeighted(5) // Limit to 5 concurrent sends
slices.Sort(recipients)
uniqueRecipients := slices.Compact(recipients)
for _, v := range uniqueRecipients {
wg.Add(1)
go func(inbox string) {
defer wg.Done()
signer, _, err := httpsig.NewSigner(algs, httpsig.DigestSha256, postHeaders, httpsig.Signature, int64(expiresIn))
if err != nil {
return
go func() {
defer func() {
if r := recover(); r != nil {
app.Logger().Error(fmt.Sprintf("Recovered from panic in PostActivity: %v", r))
}
}()
if err := sem.Acquire(context.Background(), 1); err != nil {
app.Logger().Error(fmt.Sprintf("Semaphore acquire failed: %s", err))
return
}
defer sem.Release(1)
encryptionKey := os.Getenv("POCKETBASE_ENCRYPTION_KEY")
if len(encryptionKey) == 0 {
app.Logger().Error("POCKETBASE_ENCRYPTION_KEY not set")
return
}
origin := os.Getenv("ORIGIN")
if origin == "" {
app.Logger().Error("ORIGIN not set")
return
}
buf := bytes.NewBuffer(body)
req, err := http.NewRequest(http.MethodPost, inbox, buf)
if err != nil {
app.Logger().Error(fmt.Sprintf("Request creation failed: %s", err))
return
}
req.Header.Add("Content-Type", "application/activity+json")
req.Header.Add("Date", strings.ReplaceAll(time.Now().UTC().Format(time.RFC1123), "UTC", "GMT"))
req.Header.Add("Host", req.Host)
algs := []httpsig.Algorithm{httpsig.RSA_SHA256}
postHeaders := []string{"(request-target)", "Date", "Digest", "Content-Type", "Host"}
expiresIn := 60
if err := signer.SignRequest(privateKey, pubID, req, body); err != nil {
app.Logger().Error(fmt.Sprintf("Signing request failed: %s", err))
return
}
body, err := jsonld.WithContext(
jsonld.IRI(pub.ActivityBaseURI),
jsonld.IRI(pub.SecurityContextURI),
).Marshal(activity)
if err != nil {
app.Logger().Error(fmt.Sprintf("Failed to marshal activity: %s", err))
return
}
resp, err := client.Do(req)
if err != nil {
app.Logger().Error(fmt.Sprintf("Error sending request to inbox %s: %s", inbox, err))
return
}
defer resp.Body.Close()
decryptedPrivateKey, err := security.Decrypt(actor.GetString("private_key"), encryptionKey)
if err != nil {
app.Logger().Error(fmt.Sprintf("Failed to decrypt key: %s", err))
return
}
privateKey, err := x509.ParsePKCS1PrivateKey(decryptedPrivateKey)
if err != nil {
app.Logger().Error(fmt.Sprintf("Failed to parse private key: %s", err))
return
}
pubID := actor.GetString("iri") + "#main-key"
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusAccepted {
body, _ := io.ReadAll(resp.Body)
app.Logger().Error(fmt.Sprintf("Inbox %s responded with %d: %s", inbox, resp.StatusCode, body))
} else {
app.Logger().Info(fmt.Sprintf("Sent %s to %s", activity.Type, inbox), "activity", activity)
}
client := &http.Client{}
sem := semaphore.NewWeighted(5)
}(v)
}
slices.Sort(recipients)
uniqueRecipients := slices.Compact(recipients)
wg.Wait()
var wg sync.WaitGroup
for _, v := range uniqueRecipients {
wg.Add(1)
go func(inbox string) {
defer wg.Done()
signer, _, err := httpsig.NewSigner(algs, httpsig.DigestSha256, postHeaders, httpsig.Signature, int64(expiresIn))
if err != nil {
app.Logger().Error(fmt.Sprintf("Signer creation failed: %s", err))
return
}
if err := sem.Acquire(context.Background(), 1); err != nil {
app.Logger().Error(fmt.Sprintf("Semaphore acquire failed: %s", err))
return
}
defer sem.Release(1)
req, err := http.NewRequest(http.MethodPost, inbox, bytes.NewBuffer(body))
if err != nil {
app.Logger().Error(fmt.Sprintf("Request creation failed: %s", err))
return
}
req.Header.Add("Content-Type", "application/activity+json")
req.Header.Add("Date", strings.ReplaceAll(time.Now().UTC().Format(time.RFC1123), "UTC", "GMT"))
req.Header.Add("Host", req.Host)
if err := signer.SignRequest(privateKey, pubID, req, body); err != nil {
app.Logger().Error(fmt.Sprintf("Signing request failed: %s", err))
return
}
resp, err := client.Do(req)
if err != nil {
app.Logger().Error(fmt.Sprintf("Error sending to inbox %s: %s", inbox, err))
return
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusAccepted {
respBody, _ := io.ReadAll(resp.Body)
app.Logger().Error(fmt.Sprintf("Inbox %s responded with %d: %s", inbox, resp.StatusCode, respBody))
} else {
app.Logger().Info(fmt.Sprintf("Sent %s to %s", activity.Type, inbox), "activity", activity)
}
}(v)
}
wg.Wait()
}()
return nil
}
func ProcessActivity(e *core.RequestEvent) error {
origin := os.Getenv("ORIGIN")
if origin == "" {
return fmt.Errorf("ORIGIN not set")
}
body, err := io.ReadAll(e.Request.Body)
if err != nil {
return err
}
var activity pub.Activity
activity.UnmarshalJSON(body)
inbox := fmt.Sprintf("%s%s", origin, e.Request.Header.Get("X-Forwarded-Path"))
userActor, err := e.App.FindFirstRecordByData("activitypub_actors", "inbox", inbox)
if err != nil {
return err
}
actor, err := e.App.FindFirstRecordByData("activitypub_actors", "iri", activity.Actor.GetID().String())
if err != nil {
if err == sql.ErrNoRows {
actor, err = GetActorByIRI(e.App, userActor, activity.Actor.GetID().String(), false)
if err != nil {
return err
}
} else {
return err
}
}
verified, err := verifySignature(e.App, e.Request, actor.GetString("public_key"))
if err != nil || !verified {
e.App.Logger().Error(err.Error())
return e.UnauthorizedError("Invalid http signature", err)
}
switch activity.Type {
case pub.FollowType:
ProcessFollowActivity(e.App, actor, activity)
case pub.AcceptType:
ProcessAcceptActivity(e.App, actor, activity)
case pub.UndoType:
ProcessUndoActivity(e.App, actor, activity)
case pub.UpdateType:
fallthrough
case pub.CreateType:
ProcessCreateOrUpdateActivity(e.App, actor, activity)
case pub.DeleteType:
ProcessDeleteActivity(e.App, actor, activity)
case pub.AnnounceType:
ProcessAnnounceActivity(e.App, actor, activity)
case pub.LikeType:
ProcessLikeActivity(e.App, actor, activity)
}
return e.JSON(http.StatusOK, nil)
}
func verifySignature(app core.App, req *http.Request, publicKeyPem string) (bool, error) {
origin := os.Getenv("ORIGIN")
if origin == "" {
return false, fmt.Errorf("ORIGIN not set")
}
block, _ := pem.Decode([]byte(publicKeyPem))
if block == nil || block.Type != "PUBLIC KEY" {
return false, fmt.Errorf("could not decode publicKeyPem to PUBLIC KEY pem block type")
}
req.URL = &url.URL{
Path: req.Header.Get("X-Forwarded-Path"),
}
url, err := url.Parse(origin)
if err != nil {
return false, err
}
req.Header.Set("Host", url.Host)
req.Host = url.Host
app.Logger().Info(req.Header.Get("signature"))
publicKey, err := x509.ParsePKIXPublicKey(block.Bytes)
if err != nil {
return false, err
}
v, err := httpsig.NewVerifier(req)
if err != nil {
return false, err
}
err = v.Verify(publicKey, httpsig.RSA_SHA256)
if err != nil {
return false, err
}
return true, nil
}

View File

@@ -1,13 +1,17 @@
package federation
import (
"context"
"crypto/x509"
"database/sql"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"os"
"pocketbase/util"
"strings"
"time"
@@ -19,6 +23,9 @@ import (
"github.com/pocketbase/pocketbase/tools/security"
)
var ErrProfilePrivate = errors.New("profile is private")
var ErrInvalidActorResponse = errors.New("invalid or incomplete actor response")
type WebfingerResponse struct {
Subject string `json:"subject"`
Links []struct {
@@ -27,24 +34,36 @@ type WebfingerResponse struct {
} `json:"links"`
}
func SplitHandle(handle string) (string, string) {
cleaned := strings.TrimPrefix(handle, "@")
cleaned = strings.TrimSpace(cleaned)
if !strings.Contains(cleaned, "@") {
return cleaned, ""
func validateActorResponse(actor *pub.Actor) error {
if actor == nil {
return ErrInvalidActorResponse
}
parts := strings.SplitN(cleaned, "@", 2)
user := parts[0]
domain := parts[1]
if actor.GetID().String() == "" {
return fmt.Errorf("%w: missing ID", ErrInvalidActorResponse)
}
return user, domain
if actor.PreferredUsername.String() == "" && actor.Name.String() == "" {
return fmt.Errorf("%w: missing username or name", ErrInvalidActorResponse)
}
if util.ItemID(actor.Inbox) == "" {
return fmt.Errorf("%w: missing inbox", ErrInvalidActorResponse)
}
if util.ItemID(actor.Outbox) == "" {
return fmt.Errorf("%w: missing outbox", ErrInvalidActorResponse)
}
if actor.PublicKey.PublicKeyPem == "" {
return fmt.Errorf("%w: missing public key", ErrInvalidActorResponse)
}
return nil
}
func GetActorByHandle(app core.App, actor *core.Record, handle string, includeFollows bool) (*core.Record, error) {
username, domain := SplitHandle(handle)
func GetActorByHandle(app core.App, ctx context.Context, handle string, includeFollows bool) (*core.Record, error) {
username, domain := util.SplitHandle(handle)
filter := "preferred_username={:username}&&"
if domain != "" {
@@ -63,7 +82,7 @@ func GetActorByHandle(app core.App, actor *core.Record, handle string, includeFo
dbActor = core.NewRecord(collection)
dbActor.Set("isLocal", false)
iri, err := iriFromHandle(domain, username)
iri, err := iriFromHandle(ctx, domain, username)
if err != nil {
return nil, err
}
@@ -73,10 +92,10 @@ func GetActorByHandle(app core.App, actor *core.Record, handle string, includeFo
return nil, err
}
return assembleActor(actor, dbActor, app, includeFollows)
return assembleActor(app, ctx, dbActor, includeFollows || dbActor.Id == "")
}
func GetActorByIRI(app core.App, actor *core.Record, iri string, includeFollows bool) (*core.Record, error) {
func GetActorByIRI(app core.App, ctx context.Context, iri string, includeFollows bool) (*core.Record, error) {
var dbActor *core.Record
dbActor, err := app.FindFirstRecordByFilter("activitypub_actors", "iri={:iri}", dbx.Params{"iri": iri})
if err != nil && err == sql.ErrNoRows {
@@ -93,33 +112,55 @@ func GetActorByIRI(app core.App, actor *core.Record, iri string, includeFollows
return nil, err
}
return assembleActor(actor, dbActor, app, includeFollows)
return assembleActor(app, ctx, dbActor, includeFollows || dbActor.Id == "")
}
func iriFromHandle(domain string, username string) (string, error) {
client := &http.Client{}
func iriFromHandle(ctx context.Context, domain string, username string) (string, error) {
client := util.SafeHTTPClient()
webfingerURL := fmt.Sprintf("http://%s/.well-known/webfinger?resource=acct:%s@%s", domain, username, domain)
resp, err := client.Get(webfingerURL)
if err != nil || resp.StatusCode != http.StatusOK {
return "", fmt.Errorf("webfinger request failed: %v", err)
u := &url.URL{
Scheme: "https",
Host: domain,
Path: "/.well-known/webfinger",
}
q := u.Query()
q.Set("resource", fmt.Sprintf("acct:%s@%s", username, domain))
u.RawQuery = q.Encode()
req, err := http.NewRequestWithContext(ctx, "GET", u.String(), nil)
if err != nil {
return "", err
}
resp, err := client.Do(req)
if err != nil {
return "", fmt.Errorf("webfinger request failed: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return "", fmt.Errorf("unexpected status: %d", resp.StatusCode)
}
limitedReader := io.LimitReader(resp.Body, 102400)
var wf WebfingerResponse
if err := json.NewDecoder(resp.Body).Decode(&wf); err != nil {
return "", err
if err := json.NewDecoder(limitedReader).Decode(&wf); err != nil {
return "", fmt.Errorf("failed to decode JSON: %w", err)
}
for _, link := range wf.Links {
if link.Rel == "self" {
if _, err := url.Parse(link.Href); err != nil {
return "", fmt.Errorf("invalid IRI in response")
}
return link.Href, nil
}
}
return "", fmt.Errorf("no iri in response")
}
func assembleActor(actor *core.Record, dbActor *core.Record, app core.App, includeFollows bool) (*core.Record, error) {
func assembleActor(app core.App, ctx context.Context, dbActor *core.Record, includeFollows bool) (*core.Record, error) {
origin := os.Getenv("ORIGIN")
if origin == "" {
return nil, fmt.Errorf("ORIGIN environment variable not set")
@@ -140,16 +181,16 @@ func assembleActor(actor *core.Record, dbActor *core.Record, app core.App, inclu
dbActor.Set("icon", fmt.Sprintf("%s/api/v1/files/users/%s/%s", origin, user.Id, user.GetString("avatar")))
}
dbActor.Set("summary", settings.GetString("bio"))
followerCount, err := app.CountRecords("follows", dbx.NewExp("followee={:user}", dbx.Params{"user": dbActor.Id}))
followerCount, err := app.CountRecords("follows", dbx.NewExp("followee={:user} AND status='accepted'", dbx.Params{"user": dbActor.Id}))
if err != nil {
return nil, err
}
dbActor.Set("followerCount", followerCount)
followingCount, err := app.CountRecords("follows", dbx.NewExp("follower={:user}", dbx.Params{"user": dbActor.Id}))
dbActor.Set("follower_count", followerCount)
followingCount, err := app.CountRecords("follows", dbx.NewExp("follower={:user} AND status='accepted'", dbx.Params{"user": dbActor.Id}))
if err != nil {
return nil, err
}
dbActor.Set("followingCount", followingCount)
dbActor.Set("following_count", followingCount)
dbActor.Set("last_fetched", time.Now())
@@ -157,16 +198,17 @@ func assembleActor(actor *core.Record, dbActor *core.Record, app core.App, inclu
result := make(map[string]interface{})
json.Unmarshal([]byte(privacy), &result)
private = result["account"] == "private"
// check that it's not our own profile
private = result["account"] == "private" && dbActor.Id != strings.TrimPrefix(ctx.Value("actor").(string), "actor:")
} else {
// check if value is still cached
twoHoursAgo := time.Now().Add(-2 * time.Hour)
if !includeFollows && dbActor.GetDateTime("last_fetched").Time().After(twoHoursAgo) {
twoHoursAgo := time.Now().UTC().Add(-2 * time.Hour)
if dbActor.GetDateTime("last_fetched").Time().After(twoHoursAgo) {
return dbActor, nil
}
pubActor, followers, following, err := fetchRemoteActor(actor, dbActor.GetString("iri"), includeFollows)
pubActor, followers, following, err := fetchRemoteActor(app, ctx, dbActor.GetString("iri"), includeFollows)
if err != nil {
if dbActor.Id != "" {
return dbActor, err
@@ -188,53 +230,55 @@ func assembleActor(actor *core.Record, dbActor *core.Record, app core.App, inclu
}
domain := strings.TrimPrefix(parsedUrl.Hostname(), "www.")
// this is a race condition that gets triggered when the profile is opened for the first time
existingActor, _ := app.FindFirstRecordByData("activitypub_actors", "iri", dbActor.GetString("iri"))
if existingActor != nil {
dbActor = existingActor
}
dbActor.Set("domain", domain)
dbActor.Set("followers", pubActor.Followers.GetID().String())
dbActor.Set("inbox", pubActor.Inbox.GetID().String())
dbActor.Set("followers", util.ItemID(pubActor.Followers))
dbActor.Set("inbox", util.ItemID(pubActor.Inbox))
dbActor.Set("iri", pubActor.GetID().String())
dbActor.Set("username", pubActor.Name.String())
dbActor.Set("preferred_username", pubActor.PreferredUsername.String())
dbActor.Set("following", pubActor.Following.GetID().String())
dbActor.Set("following", util.ItemID(pubActor.Following))
dbActor.Set("summary", pubActor.Summary.String())
dbActor.Set("outbox", pubActor.Outbox.GetID().String())
dbActor.Set("outbox", util.ItemID(pubActor.Outbox))
dbActor.Set("icon", icon)
dbActor.Set("published", pubActor.Published.String())
dbActor.Set("public_key", pubActor.PublicKey.PublicKeyPem)
dbActor.Set("last_fetched", time.Now())
if includeFollows {
dbActor.Set("followerCount", int(followers.TotalItems))
dbActor.Set("followingCount", int(following.TotalItems))
dbActor.Set("follower_count", int(followers.TotalItems))
dbActor.Set("following_count", int(following.TotalItems))
}
}
err := app.Save(dbActor)
if err != nil && err.Error() == "iri: Value must be unique." {
dbActor, err = app.FindFirstRecordByData("activitypub_actors", "iri", dbActor.GetString("iri"))
if err != nil {
return nil, err
}
return dbActor, nil
} else if err != nil {
if err != nil {
return nil, err
}
if private {
return dbActor, fmt.Errorf("profile is private")
return dbActor, ErrProfilePrivate
}
return dbActor, nil
}
// Fetches an AP actor and optionally followers/following collections
func fetchRemoteActor(actor *core.Record, iri string, includeFollows bool) (*pub.Actor, *pub.OrderedCollection, *pub.OrderedCollection, error) {
func fetchRemoteActor(app core.App, ctx context.Context, iri string, includeFollows bool) (*pub.Actor, *pub.OrderedCollection, *pub.OrderedCollection, error) {
encryptionKey := os.Getenv("POCKETBASE_ENCRYPTION_KEY")
if len(encryptionKey) == 0 {
return nil, nil, nil, fmt.Errorf("POCKETBASE_ENCRYPTION_KEY not set")
}
client := &http.Client{}
req, _ := http.NewRequest("GET", iri, nil)
client := util.SafeHTTPClient()
req, err := http.NewRequestWithContext(ctx, "GET", iri, nil)
headers := map[string]string{
"Accept": "application/ld+json",
@@ -247,8 +291,11 @@ func fetchRemoteActor(actor *core.Record, iri string, includeFollows bool) (*pub
req.Header.Add(k, v)
}
dbPrivateKey := actor.GetString("private_key")
if dbPrivateKey != "" {
userActorId := strings.TrimPrefix(ctx.Value("actor").(string), "actor:")
userActor, err := app.FindRecordById("activitypub_actors", userActorId)
if userActor != nil && userActor.GetString("private_key") != "" {
dbPrivateKey := userActor.GetString("private_key")
algs := []httpsig.Algorithm{httpsig.RSA_SHA256}
postHeaders := []string{"(request-target)", "Date", "Digest", "Content-Type", "Host"}
expiresIn := 60
@@ -267,7 +314,7 @@ func fetchRemoteActor(actor *core.Record, iri string, includeFollows bool) (*pub
return nil, nil, nil, err
}
pubID := actor.GetString("iri") + "#main-key"
pubID := userActor.GetString("iri") + "#main-key"
if err := signer.SignRequest(privateKey, pubID, req, []byte{}); err != nil {
return nil, nil, nil, err
@@ -289,16 +336,21 @@ func fetchRemoteActor(actor *core.Record, iri string, includeFollows bool) (*pub
return nil, nil, nil, err
}
// Validate actor response has required fields
if err := validateActorResponse(&pubActor); err != nil {
return nil, nil, nil, fmt.Errorf("actor validation failed for %s: %w", iri, err)
}
var followers, following pub.OrderedCollection
if includeFollows {
// Fetch followers
if data, err := FetchCollection(actor, pubActor.Followers.GetID().String()); err == nil {
if data, err := FetchCollection(app, ctx, util.ItemID(pubActor.Followers)); err == nil {
followers = *data
}
// Fetch following
if data, err := FetchCollection(actor, pubActor.Following.GetID().String()); err == nil {
if data, err := FetchCollection(app, ctx, util.ItemID(pubActor.Following)); err == nil {
following = *data
}
}
@@ -306,13 +358,13 @@ func fetchRemoteActor(actor *core.Record, iri string, includeFollows bool) (*pub
return &pubActor, &followers, &following, nil
}
func FetchCollection(actor *core.Record, url string) (*pub.OrderedCollection, error) {
func FetchCollection(app core.App, ctx context.Context, collectionURL string) (*pub.OrderedCollection, error) {
encryptionKey := os.Getenv("POCKETBASE_ENCRYPTION_KEY")
if len(encryptionKey) == 0 {
return nil, fmt.Errorf("POCKETBASE_ENCRYPTION_KEY not set")
}
req, _ := http.NewRequest("GET", url, nil)
req, err := http.NewRequestWithContext(ctx, "GET", collectionURL, nil)
headers := map[string]string{
"Accept": "application/ld+json",
@@ -324,38 +376,48 @@ func FetchCollection(actor *core.Record, url string) (*pub.OrderedCollection, er
for k, v := range headers {
req.Header.Add(k, v)
}
userActorId := strings.TrimPrefix(ctx.Value("actor").(string), "actor:")
userActor, err := app.FindRecordById("activitypub_actors", userActorId)
if userActor != nil && userActor.GetString("private_key") != "" {
dbPrivateKey := userActor.GetString("private_key")
if dbPrivateKey != "" {
algs := []httpsig.Algorithm{httpsig.RSA_SHA256}
postHeaders := []string{"(request-target)", "Date", "Digest", "Content-Type", "Host"}
expiresIn := 60
dbPrivateKey := actor.GetString("private_key")
if dbPrivateKey != "" {
algs := []httpsig.Algorithm{httpsig.RSA_SHA256}
postHeaders := []string{"(request-target)", "Date", "Digest", "Content-Type", "Host"}
expiresIn := 60
signer, _, err := httpsig.NewSigner(algs, httpsig.DigestSha256, postHeaders, httpsig.Signature, int64(expiresIn))
if err != nil {
return nil, err
}
decryptedPrivateKey, err := security.Decrypt(dbPrivateKey, encryptionKey)
if err != nil {
return nil, err
}
privateKey, err := x509.ParsePKCS1PrivateKey(decryptedPrivateKey)
if err != nil {
return nil, err
}
pubID := userActor.GetString("iri") + "#main-key"
if err := signer.SignRequest(privateKey, pubID, req, []byte{}); err != nil {
return nil, err
}
signer, _, err := httpsig.NewSigner(algs, httpsig.DigestSha256, postHeaders, httpsig.Signature, int64(expiresIn))
if err != nil {
return nil, err
}
decryptedPrivateKey, err := security.Decrypt(dbPrivateKey, encryptionKey)
if err != nil {
return nil, err
}
privateKey, err := x509.ParsePKCS1PrivateKey(decryptedPrivateKey)
if err != nil {
return nil, err
}
pubID := actor.GetString("iri") + "#main-key"
if err := signer.SignRequest(privateKey, pubID, req, []byte{}); err != nil {
return nil, err
}
}
resp, err := http.DefaultClient.Do(req)
if err != nil || resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("collection fetch failed for %s: %v", url, err)
client := util.SafeHTTPClient()
resp, err := client.Do(req)
if err != nil {
return nil, fmt.Errorf("collection fetch failed for %s: %v", collectionURL, err)
}
if resp.StatusCode != http.StatusOK {
if resp.StatusCode == http.StatusNotFound {
return nil, ErrProfilePrivate
}
return nil, fmt.Errorf("collection fetch %s returned: %v", collectionURL, resp.StatusCode)
}
defer resp.Body.Close()

View File

@@ -21,7 +21,7 @@ import (
"golang.org/x/net/html"
)
func CreateTrailActivity(app core.App, actor *core.Record, trail *core.Record, typ pub.ActivityVocabularyType) error {
func CreateTrailActivity(app core.App, ctx context.Context, trail *core.Record, typ pub.ActivityVocabularyType) error {
if !trail.GetBool("public") {
// only broadcast the trail if it is public
return nil
@@ -46,7 +46,7 @@ func CreateTrailActivity(app core.App, actor *core.Record, trail *core.Record, t
id := fmt.Sprintf("%s/api/v1/activitypub/activity/%s", origin, recordId)
to := "https://www.w3.org/ns/activitystreams#Public"
mentionedActors, handles, err := ActorsFromMentions(app, actor, trail.GetString("description"))
mentionedActors, err := ActorsFromMentions(app, ctx, trail.GetString("description"))
if err != nil {
return err
}
@@ -54,11 +54,11 @@ func CreateTrailActivity(app core.App, actor *core.Record, trail *core.Record, t
mentions := []string{}
cc := pub.ItemCollection{pub.IRI(trailAuthor.GetString("followers"))}
tags := pub.ItemCollection{}
for i, m := range mentionedActors {
for _, m := range mentionedActors {
inbox := m.GetString("inbox")
mention := pub.MentionNew(pub.IRI(m.GetString("iri")))
mention.Href = pub.IRI(m.GetString("iri"))
mention.Name = pub.NaturalLanguageValuesNew(pub.LangRefValueNew(pub.NilLangRef, handles[i]))
mention.Name = pub.NaturalLanguageValuesNew(pub.LangRefValueNew(pub.NilLangRef, fmt.Sprintf("@%s@%s", m.GetString("preferred_username"), m.GetString("domain"))))
tags.Append(mention)
mentions = append(mentions, inbox)
@@ -108,7 +108,7 @@ func CreateTrailActivity(app core.App, actor *core.Record, trail *core.Record, t
return PostActivity(app, trailAuthor, activity, recipients)
}
func CreateCommentActivity(app core.App, actor *core.Record, comment *core.Record, typ pub.ActivityVocabularyType) error {
func CreateCommentActivity(app core.App, ctx context.Context, comment *core.Record, typ pub.ActivityVocabularyType) error {
origin := os.Getenv("ORIGIN")
if origin == "" {
return fmt.Errorf("ORIGIN not set")
@@ -134,16 +134,16 @@ func CreateCommentActivity(app core.App, actor *core.Record, comment *core.Recor
id := fmt.Sprintf("%s/api/v1/activitypub/activity/%s", origin, activityRecordId)
to := "https://www.w3.org/ns/activitystreams#Public"
mentionedActors, handles, err := ActorsFromMentions(app, actor, comment.GetString("text"))
mentionedActors, err := ActorsFromMentions(app, ctx, comment.GetString("text"))
if err != nil {
return err
}
recipients := []string{}
tags := pub.ItemCollection{}
for i, m := range mentionedActors {
for _, m := range mentionedActors {
mention := pub.MentionNew(pub.IRI(m.GetString("iri")))
mention.Href = pub.IRI(m.GetString("iri"))
mention.Name = pub.NaturalLanguageValuesNew(pub.LangRefValueNew(pub.NilLangRef, handles[i]))
mention.Name = pub.NaturalLanguageValuesNew(pub.LangRefValueNew(pub.NilLangRef, fmt.Sprintf("@%s@%s", m.GetString("preferred_username"), m.GetString("domain"))))
tags.Append(mention)
recipients = append(recipients, m.GetString("inbox"))
@@ -193,7 +193,7 @@ func CreateCommentActivity(app core.App, actor *core.Record, comment *core.Recor
}
func CreateSummitLogActivity(app core.App, actor *core.Record, summitLog *core.Record, typ pub.ActivityVocabularyType) error {
func CreateSummitLogActivity(app core.App, ctx context.Context, summitLog *core.Record, typ pub.ActivityVocabularyType) error {
origin := os.Getenv("ORIGIN")
if origin == "" {
@@ -245,7 +245,7 @@ func CreateSummitLogActivity(app core.App, actor *core.Record, summitLog *core.R
to.Append(pub.IRI(summitLogTrailAuthor.GetString("iri")))
}
mentionedActors, handles, err := ActorsFromMentions(app, actor, summitLog.GetString("text"))
mentionedActors, err := ActorsFromMentions(app, ctx, summitLog.GetString("text"))
if err != nil {
return err
}
@@ -253,11 +253,11 @@ func CreateSummitLogActivity(app core.App, actor *core.Record, summitLog *core.R
mentions := []string{}
cc := pub.ItemCollection{pub.IRI(summitLogAuthor.GetString("followers"))}
mentionTags := pub.ItemCollection{}
for i, m := range mentionedActors {
for _, m := range mentionedActors {
inbox := m.GetString("inbox")
mention := pub.MentionNew(pub.IRI(m.GetString("iri")))
mention.Href = pub.IRI(m.GetString("iri"))
mention.Name = pub.NaturalLanguageValuesNew(pub.LangRefValueNew(pub.NilLangRef, handles[i]))
mention.Name = pub.NaturalLanguageValuesNew(pub.LangRefValueNew(pub.NilLangRef, fmt.Sprintf("@%s@%s", m.GetString("preferred_username"), m.GetString("domain"))))
mentionTags.Append(mention)
mentions = append(mentions, inbox)
@@ -444,15 +444,15 @@ func CreateListActivity(app core.App, list *core.Record, typ pub.ActivityVocabul
return app.Save(record)
}
func ProcessCreateOrUpdateActivity(app core.App, actor *core.Record, activity pub.Activity) error {
func ProcessCreateOrUpdateActivity(app core.App, actor *core.Record, recipient *core.Record, activity pub.Activity) error {
var err error
if strings.Contains(activity.Object.GetID().String(), "/api/v1/trail") {
err = processCreateOrUpdateTrailActivity(activity, app, actor)
err = processCreateOrUpdateTrailActivity(activity, app, actor, recipient)
} else if strings.Contains(activity.Object.GetID().String(), "/api/v1/summit-log") {
err = processCreateOrUpdateSummitLogActivity(activity, app, actor)
} else if strings.Contains(activity.Object.GetID().String(), "/api/v1/list") {
err = processCreateOrUpdateListActivity(activity, app, actor)
err = processCreateOrUpdateListActivity(activity, app, actor, recipient)
} else {
err = processCreateOrUpdateCommentActivity(activity, app, actor)
}
@@ -465,14 +465,16 @@ func ProcessCreateOrUpdateActivity(app core.App, actor *core.Record, activity pu
}
func processCreateOrUpdateTrailActivity(activity pub.Activity, app core.App, actor *core.Record) error {
// no need to do anything if the actor is local
if actor.GetBool("isLocal") {
return nil
func processCreateOrUpdateTrailActivity(activity pub.Activity, app core.App, actor *core.Record, recipient *core.Record) error {
trail, err := util.TrailFromActivity(activity, app, actor)
if err != nil {
return err
}
trail, err := util.TrailFromActivity(activity, app, actor)
_, err = util.InsertIntoFeed(app, recipient.Id, actor.Id, trail.Id, util.TrailFeed)
if err != nil {
return err
}
trailObject, _ := pub.ToObject(activity.Object)
@@ -791,22 +793,24 @@ func processCreateOrUpdateSummitLogActivity(activity pub.Activity, app core.App,
return nil
}
func processCreateOrUpdateListActivity(activity pub.Activity, app core.App, actor *core.Record) error {
// no need to do anything if the actor is local
if actor.GetBool("isLocal") {
return nil
func processCreateOrUpdateListActivity(activity pub.Activity, app core.App, actor *core.Record, recipient *core.Record) error {
list, err := util.ListFromActivity(activity, app, actor)
if err != nil {
return err
}
_, err := util.ListFromActivity(activity, app, actor)
_, err = util.InsertIntoFeed(app, recipient.Id, actor.Id, list.Id, util.ListFeed)
if err != nil {
return err
}
return err
}
func ActorsFromMentions(app core.App, actor *core.Record, htmlStr string) ([]*core.Record, []string, error) {
func ActorsFromMentions(app core.App, ctx context.Context, htmlStr string) ([]*core.Record, error) {
doc, err := html.Parse(strings.NewReader(htmlStr))
if err != nil {
return nil, nil, err
return nil, err
}
var handles []string
@@ -838,12 +842,12 @@ func ActorsFromMentions(app core.App, actor *core.Record, htmlStr string) ([]*co
f(doc)
for _, h := range handles {
actor, err := GetActorByHandle(app, actor, h, false)
actor, err := GetActorByHandle(app, ctx, h, false)
if err != nil {
continue
}
actors = append(actors, actor)
}
return actors, handles, nil
return actors, nil
}

View File

@@ -2,9 +2,8 @@ package federation
import (
"fmt"
"net/url"
"os"
"path"
"pocketbase/util"
"strings"
"time"
@@ -16,7 +15,10 @@ import (
)
func CreateTrailDeleteActivity(app core.App, r *core.Record) error {
if !r.GetBool("public") {
// only broadcast the trail if it is public
return nil
}
origin := os.Getenv("ORIGIN")
if origin == "" {
return fmt.Errorf("ORIGIN not set")
@@ -315,16 +317,17 @@ func ProcessDeleteActivity(app core.App, actor *core.Record, activity pub.Activi
func processDeleteTrailActivity(app core.App, activity pub.Activity) error {
trailUrl, err := url.Parse(activity.Object.GetID().String())
object := activity.Object.GetID().String()
trail, err := app.FindFirstRecordByData("trails", "iri", object)
if err != nil {
return err
}
recordId := path.Base(trailUrl.Path)
trail, err := app.FindRecordById("trails", recordId)
err = util.DeleteFromFeed(app, trail.Id)
if err != nil {
return err
}
return app.Delete(trail)
}
@@ -370,8 +373,10 @@ func processDeleteListActivity(app core.App, actor *core.Record, activity pub.Ac
return err
}
if list.GetString("author") != actor.Id {
return fmt.Errorf("actor is not summit log author")
err = util.DeleteFromFeed(app, list.Id)
if err != nil {
return err
}
return app.Delete(list)
}

View File

@@ -1,64 +1,58 @@
module pocketbase
go 1.23.0
toolchain go1.24.1
go 1.25.0
require (
github.com/meilisearch/meilisearch-go v0.29.0
github.com/pocketbase/dbx v1.11.0
github.com/pocketbase/pocketbase v0.26.1
github.com/go-ap/jsonld v0.0.0-20250905102310-8480b0fe24d9
github.com/meilisearch/meilisearch-go v0.36.2
github.com/pocketbase/dbx v1.12.0
github.com/pocketbase/pocketbase v0.38.0
github.com/tkrajina/gpxgo v1.4.0
)
require (
git.sr.ht/~mariusor/go-xsd-duration v0.0.0-20220703122237-02e73435a078 // indirect
github.com/aymerick/douceur v0.2.0 // indirect
github.com/go-ap/errors v0.0.0-20250409143711-5686c11ae650 // indirect
github.com/go-ap/jsonld v0.0.0-20221030091449-f2a191312c73 // indirect
github.com/go-sql-driver/mysql v1.8.1 // indirect
github.com/golang-jwt/jwt/v5 v5.2.1 // indirect
github.com/fsnotify/fsnotify v1.7.0 // indirect
github.com/go-ap/errors v0.0.0-20250905102357-4480b47a00c4 // indirect
github.com/go-sql-driver/mysql v1.9.3 // indirect
github.com/golang-jwt/jwt/v5 v5.3.1 // indirect
github.com/gorilla/css v1.0.1 // indirect
github.com/twpayne/go-geom v1.6.1 // indirect
github.com/valyala/fastjson v1.6.4 // indirect
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 // indirect
github.com/valyala/fastjson v1.6.10 // indirect
)
require (
github.com/andybalholm/brotli v1.1.0 // indirect
github.com/andybalholm/brotli v1.2.0 // indirect
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect
github.com/disintegration/imaging v1.6.2 // indirect
github.com/domodwyer/mailyak/v3 v3.6.2 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/fatih/color v1.18.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.8 // indirect
github.com/ganigeorgiev/fexpr v0.4.1 // indirect
github.com/go-ap/activitypub v0.0.0-20250409143848-7113328b1f3d
github.com/fatih/color v1.19.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.13 // indirect
github.com/ganigeorgiev/fexpr v0.5.0 // indirect
github.com/go-ap/activitypub v0.0.0-20250905102448-e9df599e4528
github.com/go-fed/httpsig v1.1.0
github.com/go-ozzo/ozzo-validation/v4 v4.3.0 // indirect
github.com/golang-jwt/jwt/v4 v4.5.1 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-isatty v0.0.22 // indirect
github.com/microcosm-cc/bluemonday v1.0.27
github.com/ncruces/go-strftime v0.1.9 // indirect
github.com/ncruces/go-strftime v1.0.0 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/spf13/cast v1.7.1
github.com/spf13/cobra v1.9.1 // indirect
github.com/spf13/pflag v1.0.6 // indirect
github.com/twpayne/go-gpx v1.5.0
github.com/spf13/cast v1.10.0
github.com/spf13/cobra v1.10.2
github.com/spf13/pflag v1.0.10 // indirect
github.com/twpayne/go-polyline v1.1.1
golang.org/x/crypto v0.37.0 // indirect
golang.org/x/image v0.25.0 // indirect
golang.org/x/net v0.39.0 // indirect
golang.org/x/oauth2 v0.28.0 // indirect
golang.org/x/sync v0.13.0 // indirect
golang.org/x/sys v0.32.0 // indirect
golang.org/x/text v0.24.0 // indirect
modernc.org/libc v1.61.13 // indirect
golang.org/x/crypto v0.50.0 // indirect
golang.org/x/image v0.39.0 // indirect
golang.org/x/net v0.53.0
golang.org/x/oauth2 v0.36.0 // indirect
golang.org/x/sync v0.20.0
golang.org/x/sys v0.43.0 // indirect
golang.org/x/text v0.36.0 // indirect
modernc.org/libc v1.72.0 // indirect
modernc.org/mathutil v1.7.1 // indirect
modernc.org/memory v1.8.2 // indirect
modernc.org/sqlite v1.36.1 // indirect
modernc.org/memory v1.11.0 // indirect
modernc.org/sqlite v1.50.0 // indirect
)

197
db/go.sum
View File

@@ -2,12 +2,8 @@ filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
git.sr.ht/~mariusor/go-xsd-duration v0.0.0-20220703122237-02e73435a078 h1:cliQ4HHsCo6xi2oWZYKWW4bly/Ory9FuTpFPRxj/mAg=
git.sr.ht/~mariusor/go-xsd-duration v0.0.0-20220703122237-02e73435a078/go.mod h1:g/V2Hjas6Z1UHUp4yIx6bATpNzJ7DYtD0FG3+xARWxs=
github.com/alecthomas/assert/v2 v2.10.0 h1:jjRCHsj6hBJhkmhznrCzoNpbA3zqy0fYiUcYZP/GkPY=
github.com/alecthomas/assert/v2 v2.10.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc=
github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=
github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
github.com/asaskevich/govalidator v0.0.0-20200108200545-475eaeb16496/go.mod h1:oGkLhpf+kjZl6xBf758TQhh5XrAeiJv/7FRz/2spLIg=
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so=
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw=
@@ -24,130 +20,127 @@ github.com/domodwyer/mailyak/v3 v3.6.2/go.mod h1:lOm/u9CyCVWHeaAmHIdF4RiKVxKUT/H
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/dvyukov/go-fuzz v0.0.0-20200318091601-be3528f3a813/go.mod h1:11Gm+ccJnvAhCNLlf5+cS9KjtbaD5I5zaZpFMsTHWTw=
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
github.com/fatih/color v1.19.0 h1:Zp3PiM21/9Ld6FzSKyL5c/BULoe/ONr9KlbYVOfG8+w=
github.com/fatih/color v1.19.0/go.mod h1:zNk67I0ZUT1bEGsSGyCZYZNrHuTkJJB+r6Q9VuMi0LE=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8=
github.com/ganigeorgiev/fexpr v0.4.1 h1:hpUgbUEEWIZhSDBtf4M9aUNfQQ0BZkGRaMePy7Gcx5k=
github.com/ganigeorgiev/fexpr v0.4.1/go.mod h1:RyGiGqmeXhEQ6+mlGdnUleLHgtzzu/VGO2WtJkF5drE=
github.com/go-ap/activitypub v0.0.0-20250409143848-7113328b1f3d h1:IWrWGnmKzpHqginJ18ljKkty/X8glxM8Mg3pk6bkb8g=
github.com/go-ap/activitypub v0.0.0-20250409143848-7113328b1f3d/go.mod h1:EUtZuXtHo4yKkTJmcbAZYW+X1G2poeT8icmBh24eq7o=
github.com/go-ap/errors v0.0.0-20250409143711-5686c11ae650 h1:tlwla5IQUea0CuktkBd2FLDwVzts4OeTWPPkhQPSK5Q=
github.com/go-ap/errors v0.0.0-20250409143711-5686c11ae650/go.mod h1:Vkh+Z3f24K8nMsJKXo1FHn5ebPsXvB/WDH5JRtYqdNo=
github.com/go-ap/jsonld v0.0.0-20221030091449-f2a191312c73 h1:GMKIYXyXPGIp+hYiWOhfqK4A023HdgisDT4YGgf99mw=
github.com/go-ap/jsonld v0.0.0-20221030091449-f2a191312c73/go.mod h1:jyveZeGw5LaADntW+UEsMjl3IlIwk+DxlYNsbofQkGA=
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
github.com/gabriel-vasile/mimetype v1.4.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM=
github.com/gabriel-vasile/mimetype v1.4.13/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
github.com/ganigeorgiev/fexpr v0.5.0 h1:XA9JxtTE/Xm+g/JFI6RfZEHSiQlk+1glLvRK1Lpv/Tk=
github.com/ganigeorgiev/fexpr v0.5.0/go.mod h1:RyGiGqmeXhEQ6+mlGdnUleLHgtzzu/VGO2WtJkF5drE=
github.com/go-ap/activitypub v0.0.0-20250905102448-e9df599e4528 h1:6CyCdRGY7rwTUjLseGcFkCwEYUYNOghvNMzxXROjU00=
github.com/go-ap/activitypub v0.0.0-20250905102448-e9df599e4528/go.mod h1:3ek8fXe976305mQAuuuRJgTZy4hmv0TSPWtmUYdGVfw=
github.com/go-ap/errors v0.0.0-20250905102357-4480b47a00c4 h1:Kpa0XF3gCAdeRSFzliN9jgg35KvsCchv3KRCRIJhi6s=
github.com/go-ap/errors v0.0.0-20250905102357-4480b47a00c4/go.mod h1:qHHr/m8ECbV2lkT1j5VaNnauKFV1nUo+i8S53Wtrb0A=
github.com/go-ap/jsonld v0.0.0-20250905102310-8480b0fe24d9 h1:gaBrU/E+usPHIafDIC2EwvZbehvgAEuu78Jk0zjxw5w=
github.com/go-ap/jsonld v0.0.0-20250905102310-8480b0fe24d9/go.mod h1:4h93IBxgfnE/DEleMLgJ/XCeu/RtQ+MUh3ucANseeXA=
github.com/go-fed/httpsig v1.1.0 h1:9M+hb0jkEICD8/cAiNqEB66R87tTINszBRTjwjQzWcI=
github.com/go-fed/httpsig v1.1.0/go.mod h1:RCMrTZvN1bJYtofsG4rd5NaO5obxQ5xBkdiS7xsT7bM=
github.com/go-ozzo/ozzo-validation/v4 v4.3.0 h1:byhDUpfEwjsVQb1vBunvIjh2BHQ9ead57VkAEY4V+Es=
github.com/go-ozzo/ozzo-validation/v4 v4.3.0/go.mod h1:2NKgrcHl3z6cJs+3Oo940FPRiTzuqKbvfrL2RxCj6Ew=
github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/golang-jwt/jwt/v4 v4.5.1 h1:JdqV9zKUdtaa9gdPlywC3aeoEsR681PlKC+4F5gQgeo=
github.com/golang-jwt/jwt/v4 v4.5.1/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo=
github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY=
github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/pprof v0.0.0-20250315033105-103756e64e1d h1:tx51Lf+wdE+aavqH8TcPJoCjTf4cE8hrMzROghCely0=
github.com/google/pprof v0.0.0-20250315033105-103756e64e1d/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/pprof v0.0.0-20260402051712-545e8a4df936 h1:EwtI+Al+DeppwYX2oXJCETMO23COyaKGP6fHVpkpWpg=
github.com/google/pprof v0.0.0-20260402051712-545e8a4df936/go.mod h1:MxpfABSjhmINe3F1It9d+8exIHFvUqtLIRCdOGNXqiI=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8=
github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0=
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/meilisearch/meilisearch-go v0.29.0 h1:HZ9NEKN59USINQ/DXJge/aaXq8IrsKbXGTdAoBaaDz4=
github.com/meilisearch/meilisearch-go v0.29.0/go.mod h1:2cRCAn4ddySUsFfNDLVPod/plRibQsJkXF/4gLhxbOk=
github.com/mattn/go-isatty v0.0.22 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw4=
github.com/mattn/go-isatty v0.0.22/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4=
github.com/meilisearch/meilisearch-go v0.36.2 h1:MYaMPCpdLh2aYPt+zK+19mLoA4dfBY3S1L7T0FADCjU=
github.com/meilisearch/meilisearch-go v0.36.2/go.mod h1:hWcR0MuWLSzHfbz9GGzIr3s9rnXLm1jqkmHkJPbUSvM=
github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk=
github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA=
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pocketbase/dbx v1.11.0 h1:LpZezioMfT3K4tLrqA55wWFw1EtH1pM4tzSVa7kgszU=
github.com/pocketbase/dbx v1.11.0/go.mod h1:xXRCIAKTHMgUCyCKZm55pUOdvFziJjQfXaWKhu2vhMs=
github.com/pocketbase/pocketbase v0.26.1 h1:0WBqIRKKPCqp+xHPVLB4fevkoT9HVlR4BSuNwAt5oJ0=
github.com/pocketbase/pocketbase v0.26.1/go.mod h1:t5y5pfnhrEg//RuSzSg0a926OLZ0oQj66jYs3BzDJwA=
github.com/pocketbase/dbx v1.12.0 h1:/oLErM+A0b4xI0PWTGPqSDVjzix48PqI/bng2l0PzoA=
github.com/pocketbase/dbx v1.12.0/go.mod h1:xXRCIAKTHMgUCyCKZm55pUOdvFziJjQfXaWKhu2vhMs=
github.com/pocketbase/pocketbase v0.38.0 h1:EwZiOpu2RwJ7O0d0+W+rUAGeXvBhH6zpZOX37wFB830=
github.com/pocketbase/pocketbase v0.38.0/go.mod h1:gxdfarbZ4gT/ivRNiIa7uJ1a64eY8yncW+NyvA31aLk=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y=
github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=
github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.3.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/twpayne/go-geom v1.6.1 h1:iLE+Opv0Ihm/ABIcvQFGIiFBXd76oBIar9drAwHFhR4=
github.com/twpayne/go-geom v1.6.1/go.mod h1:Kr+Nly6BswFsKM5sd31YaoWS5PeDDH2NftJTK7Gd028=
github.com/twpayne/go-gpx v1.5.0 h1:HvFSJ+0r0sbhOQ8mTvd0/n0FhcgjTFsKQGG6o7PV6G4=
github.com/twpayne/go-gpx v1.5.0/go.mod h1:vjvu/125399qj6k+px2v2v8dm08DM4I4dFBJmHHt2TE=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/tkrajina/gpxgo v1.4.0 h1:cSD5uSwy3VZuNFieTEZLyRnuIwhonQEkGPkPGW4XNag=
github.com/tkrajina/gpxgo v1.4.0/go.mod h1:BXSMfUAvKiEhMEXAFM2NvNsbjsSvp394mOvdcNjettg=
github.com/twpayne/go-polyline v1.1.1 h1:/tSF1BR7rN4HWj4XKqvRUNrCiYVMCvywxTFVofvDV0w=
github.com/twpayne/go-polyline v1.1.1/go.mod h1:ybd9IWWivW/rlXPXuuckeKUyF3yrIim+iqA7kSl4NFY=
github.com/valyala/fastjson v1.6.4 h1:uAUNq9Z6ymTgGhcm0UynUAB6tlbakBrz6CQFax3BXVQ=
github.com/valyala/fastjson v1.6.4/go.mod h1:CLCAqky6SMuOcxStkYQvblddUtoRxhYMGLrsQns1aXY=
github.com/valyala/fastjson v1.6.10 h1:/yjJg8jaVQdYR3arGxPE2X5z89xrlhS0eGXdv+ADTh4=
github.com/valyala/fastjson v1.6.10/go.mod h1:e6FubmQouUNP73jtMLmcbxS6ydWIpOfhz34TSfO3JaE=
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE=
golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc=
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 h1:nDVHiLt8aIbd/VzvPWN6kSOPE7+F/fNFDSXLVYkE/Iw=
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394/go.mod h1:sIifuuw/Yco/y6yb6+bDNfyeQ/MdPUy/hKEMYQV17cM=
golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI=
golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q=
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.25.0 h1:Y6uW6rH1y5y/LK1J8BPWZtr6yZ7hrsy6hFrXjgsc2fQ=
golang.org/x/image v0.25.0/go.mod h1:tCAmOEGthTtkalusGp1g3xa2gke8J6c2N565dTyl9Rs=
golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU=
golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
golang.org/x/image v0.39.0 h1:skVYidAEVKgn8lZ602XO75asgXBgLj9G/FE3RbuPFww=
golang.org/x/image v0.39.0/go.mod h1:sIbmppfU+xFLPIG0FoVUTvyBMmgng1/XAMhQ2ft0hpA=
golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI=
golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY=
golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E=
golang.org/x/oauth2 v0.28.0 h1:CrgCKl8PPAVtLnU3c+EDw6x11699EWlsDeWNWKdIOkc=
golang.org/x/oauth2 v0.28.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8=
golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610=
golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA=
golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs=
golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=
golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.42.0 h1:UiKe+zDFmJobeJ5ggPwOshJIVt6/Ft0rcfrXZDLWAWY=
golang.org/x/term v0.42.0/go.mod h1:Dq/D+snpsbazcBG5+F9Q1n2rXV8Ma+71xEjTRufARgY=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg=
golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.31.0 h1:0EedkvKDbh+qistFTd0Bcwe/YLh4vHwWEkiI0toFIBU=
golang.org/x/tools v0.31.0/go.mod h1:naFTU+Cev749tSJRXJlna0T3WxKvb1kWEx15xA4SdmQ=
golang.org/x/tools v0.43.0 h1:12BdW9CeB3Z+J/I/wj34VMl8X+fEXBxVR90JeMX5E7s=
golang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0=
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
@@ -155,26 +148,30 @@ gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
modernc.org/cc/v4 v4.24.4 h1:TFkx1s6dCkQpd6dKurBNmpo+G8Zl4Sq/ztJ+2+DEsh0=
modernc.org/cc/v4 v4.24.4/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
modernc.org/ccgo/v4 v4.23.16 h1:Z2N+kk38b7SfySC1ZkpGLN2vthNJP1+ZzGZIlH7uBxo=
modernc.org/ccgo/v4 v4.23.16/go.mod h1:nNma8goMTY7aQZQNTyN9AIoJfxav4nvTnvKThAeMDdo=
modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE=
modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ=
modernc.org/gc/v2 v2.6.3 h1:aJVhcqAte49LF+mGveZ5KPlsp4tdGdAOT4sipJXADjw=
modernc.org/gc/v2 v2.6.3/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
modernc.org/libc v1.61.13 h1:3LRd6ZO1ezsFiX1y+bHd1ipyEHIJKvuprv0sLTBwLW8=
modernc.org/libc v1.61.13/go.mod h1:8F/uJWL/3nNil0Lgt1Dpz+GgkApWh04N3el3hxJcA6E=
modernc.org/cc/v4 v4.27.3 h1:uNCgn37E5U09mTv1XgskEVUJ8ADKpmFMPxzGJ0TSo+U=
modernc.org/cc/v4 v4.27.3/go.mod h1:3YjcbCqhoTTHPycJDRl2WZKKFj0nwcOIPBfEZK0Hdk8=
modernc.org/ccgo/v4 v4.32.4 h1:L5OB8rpEX4ZsXEQwGozRfJyJSFHbbNVOoQ59DU9/KuU=
modernc.org/ccgo/v4 v4.32.4/go.mod h1:lY7f+fiTDHfcv6YlRgSkxYfhs+UvOEEzj49jAn2TOx0=
modernc.org/fileutil v1.4.0 h1:j6ZzNTftVS054gi281TyLjHPp6CPHr2KCxEXjEbD6SM=
modernc.org/fileutil v1.4.0/go.mod h1:EqdKFDxiByqxLk8ozOxObDSfcVOv/54xDs/DUHdvCUU=
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
modernc.org/gc/v3 v3.1.2 h1:ZtDCnhonXSZexk/AYsegNRV1lJGgaNZJuKjJSWKyEqo=
modernc.org/gc/v3 v3.1.2/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY=
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
modernc.org/libc v1.72.0 h1:IEu559v9a0XWjw0DPoVKtXpO2qt5NVLAnFaBbjq+n8c=
modernc.org/libc v1.72.0/go.mod h1:tTU8DL8A+XLVkEY3x5E/tO7s2Q/q42EtnNWda/L5QhQ=
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
modernc.org/memory v1.8.2 h1:cL9L4bcoAObu4NkxOlKWBWtNHIsnnACGF/TbqQ6sbcI=
modernc.org/memory v1.8.2/go.mod h1:ZbjSvMO5NQ1A2i3bWeDiVMxIorXwdClKE/0SZ+BMotU=
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
modernc.org/sqlite v1.36.1 h1:bDa8BJUH4lg6EGkLbahKe/8QqoF8p9gArSc6fTqYhyQ=
modernc.org/sqlite v1.36.1/go.mod h1:7MPwH7Z6bREicF9ZVUR78P1IKuxfZ8mRIDHD0iD+8TU=
modernc.org/sqlite v1.50.0 h1:eMowQSWLK0MeiQTdmz3lqoF5dqclujdlIKeJA11+7oM=
modernc.org/sqlite v1.50.0/go.mod h1:m0w8xhwYUVY3H6pSDwc3gkJ/irZT/0YEXwBlhaxQEew=
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=

22
db/hooks/api_tokens.go Normal file
View File

@@ -0,0 +1,22 @@
package hooks
import (
"github.com/pocketbase/pocketbase/core"
"github.com/pocketbase/pocketbase/tools/security"
)
func CreateAPITokenHandler() func(e *core.RecordEvent) error {
return func(e *core.RecordEvent) error {
rawToken := "wanderer_key_" + security.RandomString(32)
hashedKey := security.SHA256(rawToken)
e.Record.Set("token", hashedKey)
// Temporarily store rawToken so we can display it once to the user
e.Record.WithCustomData(true)
e.Record.Set("rawToken", rawToken)
return e.Next()
}
}

47
db/hooks/bootstrap.go Normal file
View File

@@ -0,0 +1,47 @@
package hooks
import (
"cmp"
"os"
"github.com/pocketbase/pocketbase/core"
"github.com/spf13/cast"
)
func OnBootstrapHandler() func(se *core.BootstrapEvent) error {
return func(e *core.BootstrapEvent) error {
if err := e.Next(); err != nil {
return err
}
if e.App.Settings().Meta.AppName == "Acme" {
e.App.Settings().Meta.AppName = "wanderer"
}
if v := os.Getenv("ORIGIN"); v != "" {
e.App.Settings().Meta.AppURL = v
}
if v := cmp.Or(os.Getenv("POCKETBASE_SMTP_SENDER_ADDRESS"), os.Getenv("POCKETBASE_SMTP_SENDER_ADRESS")); v != "" {
e.App.Settings().Meta.SenderAddress = v
}
if v := os.Getenv("POCKETBASE_SMTP_SENDER_NAME"); v != "" {
e.App.Settings().Meta.SenderName = v
}
if v := os.Getenv("POCKETBASE_SMTP_ENABLED"); v != "" {
e.App.Settings().SMTP.Enabled = cast.ToBool(v)
}
if v := os.Getenv("POCKETBASE_SMTP_HOST"); v != "" {
e.App.Settings().SMTP.Host = v
}
if v := os.Getenv("POCKETBASE_SMTP_PORT"); v != "" {
e.App.Settings().SMTP.Port = cast.ToInt(v)
}
if v := os.Getenv("POCKETBASE_SMTP_USERNAME"); v != "" {
e.App.Settings().SMTP.Username = v
}
if v := os.Getenv("POCKETBASE_SMTP_PASSWORD"); v != "" {
e.App.Settings().SMTP.Password = v
}
return e.App.Save(e.App.Settings())
}
}

65
db/hooks/comments.go Normal file
View File

@@ -0,0 +1,65 @@
package hooks
import (
"pocketbase/federation"
"pocketbase/util"
pub "github.com/go-ap/activitypub"
"github.com/meilisearch/meilisearch-go"
"github.com/pocketbase/pocketbase/core"
)
func CreateCommentHandler() func(e *core.RecordRequestEvent) error {
return func(e *core.RecordRequestEvent) error {
e.Next()
userActor, err := e.App.FindFirstRecordByData("activitypub_actors", "user", e.Auth.Id)
if err != nil {
return err
}
ctx, err := util.GetSafeActorContext(e.Request, userActor)
if err != nil {
return err
}
err = federation.CreateCommentActivity(e.App, ctx, e.Record, pub.CreateType)
if err != nil {
return err
}
return nil
}
}
func UpdateCommentHandler() func(e *core.RecordRequestEvent) error {
return func(e *core.RecordRequestEvent) error {
userActor, err := e.App.FindFirstRecordByData("activitypub_actors", "user", e.Auth.Id)
if err != nil {
return err
}
ctx, err := util.GetSafeActorContext(e.Request, userActor)
if err != nil {
return err
}
err = federation.CreateCommentActivity(e.App, ctx, e.Record, pub.UpdateType)
if err != nil {
return err
}
return e.Next()
}
}
func DeleteCommentHandler(client meilisearch.ServiceManager) func(e *core.RecordRequestEvent) error {
return func(e *core.RecordRequestEvent) error {
err := federation.CreateCommentDeleteActivity(e.App, client, e.Record)
if err != nil {
return err
}
return e.Next()
}
}

57
db/hooks/feed.go Normal file
View File

@@ -0,0 +1,57 @@
package hooks
import (
"fmt"
"pocketbase/util"
"strings"
"github.com/pocketbase/pocketbase/core"
)
func ListFeedHandler() func(e *core.RecordsListRequestEvent) error {
return func(e *core.RecordsListRequestEvent) error {
for _, r := range e.Records {
var item *core.Record
var err error
typ := r.GetString("type")
typ = strings.Trim(typ, "\"")
itemId := r.GetString("item")
itemId = strings.Trim(itemId, "\"")
switch typ {
case string(util.TrailFeed):
item, err = e.App.FindRecordById("trails", itemId)
case string(util.ListFeed):
item, err = e.App.FindRecordById("lists", itemId)
case string(util.SummitLogFeed):
item, err = e.App.FindRecordById("summit_logs", itemId)
}
if err != nil {
continue
}
if item == nil {
continue
}
errs := e.App.ExpandRecord(item, []string{"author"}, nil)
if len(errs) > 0 {
return fmt.Errorf("failed to expand author: %v", errs)
}
if typ == string(util.TrailFeed) {
errs := e.App.ExpandRecord(item, []string{"category"}, nil)
if len(errs) > 0 {
return fmt.Errorf("failed to expand category: %v", errs)
}
}
r.MergeExpand(map[string]any{"item": item})
}
return e.Next()
}
}

24
db/hooks/follow.go Normal file
View File

@@ -0,0 +1,24 @@
package hooks
import (
"pocketbase/federation"
"github.com/pocketbase/pocketbase/core"
)
func CreateFollowHandler() func(e *core.RecordRequestEvent) error {
return func(e *core.RecordRequestEvent) error {
e.Next()
federation.CreateFollowActivity(e.App, e.Record)
return nil
}
}
func DeleteFollowHandler() func(e *core.RecordRequestEvent) error {
return func(e *core.RecordRequestEvent) error {
federation.CreateUnfollowActivity(e.App, e.Record)
return e.Next()
}
}

146
db/hooks/integrations.go Normal file
View File

@@ -0,0 +1,146 @@
package hooks
import (
"encoding/json"
"os"
"pocketbase/util"
"github.com/pocketbase/pocketbase/apis"
"github.com/pocketbase/pocketbase/core"
"github.com/pocketbase/pocketbase/tools/security"
)
func ListIntegrationHandler() func(e *core.RecordsListRequestEvent) error {
return func(e *core.RecordsListRequestEvent) error {
if e.HasSuperuserAuth() {
return e.Next()
}
for _, r := range e.Records {
err := censorIntegrationSecrets(r)
if err != nil {
return err
}
}
return e.Next()
}
}
func CreateIntegrationHandler() func(e *core.RecordEvent) error {
return func(e *core.RecordEvent) error {
err := encryptIntegrationSecrets(e.App, e.Record)
if err != nil {
return err
}
return e.Next()
}
}
func CreateUpdateIntegrationSuccessHandler() func(e *core.RecordEvent) error {
return func(e *core.RecordEvent) error {
err := censorIntegrationSecrets(e.Record)
if err != nil {
return err
}
return e.Next()
}
}
func UpdateIntegrationHandler() func(e *core.RecordEvent) error {
return func(e *core.RecordEvent) error {
err := encryptIntegrationSecrets(e.App, e.Record)
if err != nil {
return err
}
return e.Next()
}
}
func censorIntegrationSecrets(r *core.Record) error {
secrets := map[string][]string{
"strava": {"clientSecret", "refreshToken", "accessToken", "expiresAt"},
"komoot": {"password"},
"hammerhead": {"password"},
}
for key, secretKeys := range secrets {
if integrationString := r.GetString(key); integrationString != "" {
var integration map[string]interface{}
if err := json.Unmarshal([]byte(integrationString), &integration); err != nil {
return err
}
if integration == nil {
continue
}
for _, secretKey := range secretKeys {
integration[secretKey] = ""
}
b, err := json.Marshal(integration)
if err != nil {
return err
}
r.Set(key, string(b))
}
}
return nil
}
func encryptIntegrationSecrets(app core.App, r *core.Record) error {
encryptionKey := os.Getenv("POCKETBASE_ENCRYPTION_KEY")
if len(encryptionKey) == 0 {
return apis.NewBadRequestError("POCKETBASE_ENCRYPTION_KEY not set", nil)
}
secrets := map[string][]string{
"strava": {"clientSecret", "refreshToken", "accessToken", "expiresAt"},
"komoot": {"password"},
"hammerhead": {"password"},
}
original, _ := app.FindRecordById("integrations", r.Id)
for key, secretKeys := range secrets {
if integrationString := r.GetString(key); integrationString != "" {
var integration map[string]interface{}
if err := json.Unmarshal([]byte(integrationString), &integration); err != nil {
return err
}
for _, secretKey := range secretKeys {
// If the secret is already encrypted, we don't re-encrypt it.
// TODO: This is a bit of a hack, we should handle this in a more robust way (e.g.
// storing flag on the record or prefixing encrypted strings with enc: or smilar).
// Doing that would also potentially allow us to support key rotation in the future.
if secret, ok := integration[secretKey].(string); ok && len(secret) > 0 && !util.CanDecryptSecret(secret) {
encryptedSecret, err := security.Encrypt([]byte(secret), encryptionKey)
if err != nil {
return err
}
integration[secretKey] = encryptedSecret
} else if original != nil {
originalString := original.GetString(key)
var originalIntegration map[string]interface{}
if err := json.Unmarshal([]byte(originalString), &originalIntegration); err != nil {
return err
}
if integration == nil {
continue
}
integration[secretKey] = originalIntegration[secretKey]
}
}
b, err := json.Marshal(integration)
if err != nil {
return err
}
r.Set(key, string(b))
}
}
return nil
}

105
db/hooks/list.go Normal file
View File

@@ -0,0 +1,105 @@
package hooks
import (
"pocketbase/federation"
"pocketbase/util"
pub "github.com/go-ap/activitypub"
"github.com/meilisearch/meilisearch-go"
"github.com/pocketbase/pocketbase/core"
)
func CreateListHandler(client meilisearch.ServiceManager) func(e *core.RecordEvent) error {
return func(e *core.RecordEvent) error {
record := e.Record
author, err := e.App.FindRecordById("activitypub_actors", record.GetString(("author")))
if err != nil {
return err
}
if err := util.IndexLists(e.App, []*core.Record{record}, client); err != nil {
return err
}
if !author.GetBool("isLocal") {
// this happens if someone fetches a remote list
// we create a stub list record for later reference
// no need to create an activity for that
return e.Next()
}
err = e.Next()
if err != nil {
return err
}
err = federation.CreateListActivity(e.App, e.Record, pub.CreateType)
if err != nil {
return err
}
_, err = util.InsertIntoFeed(e.App, author.Id, author.Id, record.Id, util.ListFeed)
if err != nil {
return err
}
return nil
}
}
func UpdateListHandler(client meilisearch.ServiceManager) func(e *core.RecordEvent) error {
return func(e *core.RecordEvent) error {
record := e.Record
author, err := e.App.FindRecordById("activitypub_actors", record.GetString(("author")))
if err != nil {
return err
}
err = util.UpdateList(e.App, record, author, client)
if err != nil {
return err
}
if !author.GetBool("isLocal") {
// this happens if someone fetches a remote list
// we create a stub list record for later reference
// no need to create an activity for that
return e.Next()
}
err = e.Next()
if err != nil {
return err
}
err = federation.CreateListActivity(e.App, e.Record, pub.CreateType)
if err != nil {
return err
}
return nil
}
}
func DeleteListHandler(client meilisearch.ServiceManager) func(e *core.RecordEvent) error {
return func(e *core.RecordEvent) error {
record := e.Record
_, err := client.Index("lists").DeleteDocument(record.Id, nil)
if err != nil {
return err
}
err = federation.CreateListDeleteActivity(e.App, record)
if err != nil {
return err
}
err = util.DeleteFromFeed(e.App, record.Id)
if err != nil {
return err
}
return e.Next()
}
}

56
db/hooks/list_share.go Normal file
View File

@@ -0,0 +1,56 @@
package hooks
import (
"pocketbase/federation"
"pocketbase/util"
"github.com/meilisearch/meilisearch-go"
"github.com/pocketbase/dbx"
"github.com/pocketbase/pocketbase/core"
)
func CreateListShareHandler(client meilisearch.ServiceManager) func(e *core.RecordRequestEvent) error {
return func(e *core.RecordRequestEvent) error {
err := e.Next()
if err != nil {
return err
}
record := e.Record
listId := record.GetString("list")
shares, err := e.App.FindAllRecords("list_share",
dbx.NewExp("list = {:listId}", dbx.Params{"listId": listId}),
)
if err != nil {
return err
}
actorIds := make([]string, len(shares))
for i, r := range shares {
actorIds[i] = r.GetString("actor")
}
err = util.UpdateListShares(listId, actorIds, client)
if err != nil {
return err
}
err = federation.CreateAnnounceActivity(e.App, record, federation.ListAnnounceType)
if err != nil {
return err
}
return nil
}
}
func DeleteListShareHandler(client meilisearch.ServiceManager) func(e *core.RecordRequestEvent) error {
return func(e *core.RecordRequestEvent) error {
record := e.Record
listId := record.GetString("list")
err := util.UpdateListShares(listId, []string{}, client)
if err != nil {
return err
}
return e.Next()
}
}

96
db/hooks/summit_logs.go Normal file
View File

@@ -0,0 +1,96 @@
package hooks
import (
"pocketbase/federation"
"pocketbase/util"
pub "github.com/go-ap/activitypub"
"github.com/meilisearch/meilisearch-go"
"github.com/pocketbase/pocketbase/core"
)
func CreateSummitLogHandler(client meilisearch.ServiceManager) func(e *core.RecordRequestEvent) error {
return func(e *core.RecordRequestEvent) error {
err := e.Next()
if err != nil {
return err
}
userActor, err := e.App.FindFirstRecordByData("activitypub_actors", "user", e.Auth.Id)
if err != nil {
return err
}
ctx, err := util.GetSafeActorContext(e.Request, userActor)
if err != nil {
return err
}
trail, err := e.App.FindRecordById("trails", e.Record.GetString("trail"))
if err != nil {
return err
}
if err := util.IndexTrails(e.App, []*core.Record{trail}, client); err != nil {
return err
}
err = federation.CreateSummitLogActivity(e.App, ctx, e.Record, pub.CreateType)
if err != nil {
return err
}
return nil
}
}
func UpdateSummitLogHandler() func(e *core.RecordRequestEvent) error {
return func(e *core.RecordRequestEvent) error {
err := e.Next()
if err != nil {
return err
}
userActor, err := e.App.FindFirstRecordByData("activitypub_actors", "user", e.Auth.Id)
if err != nil {
return err
}
ctx, err := util.GetSafeActorContext(e.Request, userActor)
if err != nil {
return err
}
err = federation.CreateSummitLogActivity(e.App, ctx, e.Record, pub.UpdateType)
if err != nil {
return err
}
return nil
}
}
func DeleteSummitLogHandler(client meilisearch.ServiceManager) func(e *core.RecordRequestEvent) error {
return func(e *core.RecordRequestEvent) error {
err := e.Next()
if err != nil {
return err
}
trail, err := e.App.FindRecordById("trails", e.Record.GetString("trail"))
if err != nil {
return err
}
if err := util.IndexTrails(e.App, []*core.Record{trail}, client); err != nil {
return err
}
err = federation.CreateSummitLogDeleteActivity(e.App, e.Record)
if err != nil {
return err
}
return nil
}
}

118
db/hooks/trail_like.go Normal file
View File

@@ -0,0 +1,118 @@
package hooks
import (
"database/sql"
"pocketbase/federation"
"pocketbase/util"
"github.com/meilisearch/meilisearch-go"
"github.com/pocketbase/dbx"
"github.com/pocketbase/pocketbase/core"
)
func CreateTrailLikeHandler(client meilisearch.ServiceManager) func(e *core.RecordEvent) error {
return func(e *core.RecordEvent) error {
err := e.Next()
if err != nil {
return err
}
record := e.Record
trailId := record.GetString("trail")
actorId := record.GetString("actor")
actor, err := e.App.FindRecordById("activitypub_actors", actorId)
if err != nil {
return err
}
trail, err := e.App.FindRecordById("trails", trailId)
if err != nil {
return err
}
likes, err := e.App.FindAllRecords("trail_like",
dbx.NewExp("trail = {:trailId}", dbx.Params{"trailId": trailId}),
)
if err != nil {
return err
}
trail.Set("like_count", len(likes))
err = e.App.UnsafeWithoutHooks().Save(trail)
if err != nil {
return err
}
actorIds := make([]string, len(likes))
for i, r := range likes {
actorIds[i] = r.GetString("actor")
}
err = util.UpdateTrailLikes(trailId, actorIds, client)
if err != nil {
return err
}
if !actor.GetBool("isLocal") {
// this happens if someone likes a remote trail
// we create a local copy
// no need to create an activity for that
return nil
}
err = federation.CreateLikeActivity(e.App, record)
if err != nil {
return err
}
return nil
}
}
func DeleteTrailLikeHandler(client meilisearch.ServiceManager) func(e *core.RecordEvent) error {
return func(e *core.RecordEvent) error {
record := e.Record
trailId := record.GetString("trail")
actorId := record.GetString("actor")
actor, err := e.App.FindRecordById("activitypub_actors", actorId)
if err != nil {
return err
}
// trail might deleted be already if this is called as part of a cascade
trail, err := e.App.FindRecordById("trails", trailId)
if err != nil && err == sql.ErrNoRows {
return nil
} else if err != nil {
return err
}
likes, err := e.App.CountRecords("trail_like", dbx.NewExp("trail={:trail}", dbx.Params{"trail": trailId}))
if err != nil {
return err
}
trail.Set("like_count", likes)
err = e.App.UnsafeWithoutHooks().Save(trail)
if err != nil {
return err
}
err = util.UpdateTrailLikes(trailId, []string{}, client)
if err != nil {
return err
}
if !actor.GetBool("isLocal") {
// this happens if someone likes a remote trail
// we create a local copy
// no need to create an activity for that
return nil
}
err = federation.CreateUnlikeActivity(e.App, record)
if err != nil {
return err
}
return e.Next()
}
}

57
db/hooks/trail_share.go Normal file
View File

@@ -0,0 +1,57 @@
package hooks
import (
"pocketbase/federation"
"pocketbase/util"
"github.com/meilisearch/meilisearch-go"
"github.com/pocketbase/dbx"
"github.com/pocketbase/pocketbase/core"
)
func CreateTrailShareHandler(client meilisearch.ServiceManager) func(e *core.RecordRequestEvent) error {
return func(e *core.RecordRequestEvent) error {
err := e.Next()
if err != nil {
return err
}
record := e.Record
trailId := record.GetString("trail")
shares, err := e.App.FindAllRecords("trail_share",
dbx.NewExp("trail = {:trailId}", dbx.Params{"trailId": trailId}),
)
if err != nil {
return err
}
actorIds := make([]string, len(shares))
for i, r := range shares {
actorIds[i] = r.GetString("actor")
}
err = util.UpdateTrailShares(trailId, actorIds, client)
if err != nil {
return err
}
err = federation.CreateAnnounceActivity(e.App, record, federation.TrailAnnounceType)
if err != nil {
return err
}
return nil
}
}
func DeleteTrailShareHandler(client meilisearch.ServiceManager) func(e *core.RecordRequestEvent) error {
return func(e *core.RecordRequestEvent) error {
record := e.Record
trailId := record.GetString("trail")
err := util.UpdateTrailShares(trailId, []string{}, client)
if err != nil {
return err
}
return e.Next()
}
}

122
db/hooks/trails.go Normal file
View File

@@ -0,0 +1,122 @@
package hooks
import (
"log"
"pocketbase/federation"
"pocketbase/util"
"time"
"github.com/go-ap/activitypub"
pub "github.com/go-ap/activitypub"
"github.com/meilisearch/meilisearch-go"
"github.com/pocketbase/pocketbase/core"
)
func CreateTrailHandler(client meilisearch.ServiceManager) func(e *core.RecordEvent) error {
return func(e *core.RecordEvent) error {
record := e.Record
userActor, err := e.App.FindRecordById("activitypub_actors", record.GetString(("author")))
if err != nil {
return err
}
if err := util.IndexTrails(e.App, []*core.Record{record}, client); err != nil {
return err
}
if !userActor.GetBool("isLocal") {
// this happens if someone fetches a remote trail
// we create a stub trail record for later reference
// no need to create an activity for that
return e.Next()
}
err = e.Next()
if err != nil {
return err
}
ctx, err := util.GetSafeActorContext(nil, userActor)
if err != nil {
return err
}
err = federation.CreateTrailActivity(e.App, ctx, e.Record, activitypub.CreateType)
if err != nil {
return err
}
_, err = util.InsertIntoFeed(e.App, userActor.Id, userActor.Id, record.Id, util.TrailFeed)
if err != nil {
return err
}
return nil
}
}
func UpdateTrailHandler(client meilisearch.ServiceManager) func(e *core.RecordEvent) error {
return func(e *core.RecordEvent) error {
record := e.Record
userActor, err := e.App.FindRecordById("activitypub_actors", record.GetString(("author")))
if err != nil {
return err
}
err = util.UpdateTrail(e.App, record, userActor, client)
if err != nil {
return err
}
if !userActor.GetBool("isLocal") {
// this happens if someone fetches a remote trail
// we create a stub trail record for later reference
// no need to create an activity for that
return e.Next()
}
err = e.Next()
if err != nil {
return err
}
ctx, err := util.GetSafeActorContext(nil, userActor)
if err != nil {
return err
}
err = federation.CreateTrailActivity(e.App, ctx, e.Record, pub.UpdateType)
if err != nil {
return err
}
return nil
}
}
func DeleteTrailHandler(client meilisearch.ServiceManager) func(e *core.RecordEvent) error {
return func(e *core.RecordEvent) error {
record := e.Record
task, err := client.Index("trails").DeleteDocument(record.Id, nil)
if err != nil {
return err
}
interval := 500 * time.Millisecond
_, err = client.WaitForTask(task.TaskUID, interval)
if err != nil {
log.Fatalf("Error waiting for task completion: %v", err)
}
err = federation.CreateTrailDeleteActivity(e.App, e.Record)
if err != nil {
return err
}
err = util.DeleteFromFeed(e.App, record.Id)
if err != nil {
return err
}
return e.Next()
}
}

101
db/hooks/users.go Normal file
View File

@@ -0,0 +1,101 @@
package hooks
import (
"fmt"
"os"
"pocketbase/util"
"github.com/meilisearch/meilisearch-go"
"github.com/pocketbase/dbx"
"github.com/pocketbase/pocketbase/core"
)
func CreateUserHandler(client meilisearch.ServiceManager) func(e *core.RecordEvent) error {
return func(e *core.RecordEvent) error {
userId := e.Record.Id
err := createDefaultUserSettings(e.App, e.Record.Id)
if err != nil {
return err
}
actor, err := util.ActorFromUser(e.App, e.Record)
if err != nil {
return err
}
searchRules := map[string]interface{}{
"lists": map[string]string{
"filter": "public = true OR author = " + actor.Id + " OR shares = " + userId,
},
"trails": map[string]string{
"filter": "public = true OR author = " + actor.Id + " OR shares = " + userId,
},
}
token, err := util.GenerateMeilisearchToken(searchRules, client)
if err != nil {
return err
}
e.Record.Set("token", token)
if err := e.App.Save(e.Record); err != nil {
return err
}
return e.Next()
}
}
func UpdateUserHandler(client meilisearch.ServiceManager) func(e *core.RecordEvent) error {
return func(e *core.RecordEvent) error {
actor, err := e.App.FindFirstRecordByData("activitypub_actors", "user", e.Record.Id)
if err != nil {
return e.Next()
}
icon := ""
origin := os.Getenv("ORIGIN")
if origin != "" && e.Record.GetString("avatar") != "" {
icon = fmt.Sprintf("%s/api/v1/files/_pb_users_auth_/%s/%s", origin, e.Record.Id, e.Record.GetString("avatar"))
}
actor.Set("icon", icon)
if err := e.App.Save(actor); err != nil {
return err
}
trails, err := e.App.FindRecordsByFilter("trails", "author={:author}", "", -1, 0, dbx.Params{"author": actor.Id})
if err != nil {
return err
}
if len(trails) > 0 {
if err := util.IndexTrails(e.App, trails, client); err != nil {
return err
}
}
lists, err := e.App.FindRecordsByFilter("lists", "author={:author}", "", -1, 0, dbx.Params{"author": actor.Id})
if err != nil {
return err
}
if len(lists) > 0 {
if err := util.IndexLists(e.App, lists, client); err != nil {
return err
}
}
return e.Next()
}
}
func createDefaultUserSettings(app core.App, userId string) error {
collection, err := app.FindCollectionByNameOrId("settings")
if err != nil {
return err
}
settings := core.NewRecord(collection)
settings.Set("language", "en")
settings.Set("unit", "metric")
settings.Set("mapFocus", "trails")
settings.Set("user", userId)
return app.Save(settings)
}

View File

@@ -0,0 +1,927 @@
package hammerhead
import (
"bytes"
"context"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"io"
"mime/multipart"
"net/http"
"math"
"os"
"slices"
"strings"
"time"
"github.com/meilisearch/meilisearch-go"
"github.com/pocketbase/dbx"
"github.com/pocketbase/pocketbase/apis"
"github.com/pocketbase/pocketbase/core"
"github.com/pocketbase/pocketbase/tools/filesystem"
"github.com/pocketbase/pocketbase/tools/security"
"github.com/tkrajina/gpxgo/gpx"
"pocketbase/services/trailmerge"
"pocketbase/util"
)
func SyncHammerhead(app core.App, client meilisearch.ServiceManager) error {
integrations, err := app.FindAllRecords("integrations", dbx.NewExp("true"))
if err != nil {
return err
}
for _, i := range integrations {
encryptionKey := os.Getenv("POCKETBASE_ENCRYPTION_KEY")
if len(encryptionKey) == 0 {
return errors.New("POCKETBASE_ENCRYPTION_KEY not set")
}
userId := i.GetString("user")
actor, err := app.FindFirstRecordByData("activitypub_actors", "user", userId)
if err != nil {
warning := fmt.Sprintf("no actor found for user: %s\n", userId)
fmt.Print(warning)
app.Logger().Warn(warning)
continue
}
ctx, err := util.GetSafeActorContext(nil, actor)
if err != nil {
continue
}
hammerheadString := i.GetString("hammerhead")
hammerheadIntegration := HammerheadIntegration{
Planned: true,
Completed: true,
Merge: trailmerge.DefaultIntegrationAutoMergeSettings(),
}
json.Unmarshal([]byte(hammerheadString), &hammerheadIntegration)
if !hammerheadIntegration.Active || hammerheadIntegration.Email == "" || hammerheadIntegration.Password == "" {
continue
}
h := &HammerheadApi{}
decryptedPassword, err := security.Decrypt(hammerheadIntegration.Password, encryptionKey)
if err != nil {
warning := fmt.Sprintf("unable to decrypt password: %v\n", err)
fmt.Print(warning)
app.Logger().Warn(warning)
continue
}
err = h.Login(hammerheadIntegration.Email, string(decryptedPassword))
if err != nil {
warning := fmt.Sprintf("Hammerhead login failed: %v\n", err)
fmt.Print(warning)
app.Logger().Warn(warning)
continue
}
page := 0
totalPages := 0
stopped := false
var after int64 = 0
if hammerheadIntegration.After != "" {
t, err := time.Parse("2006-01-02", hammerheadIntegration.After)
if err != nil {
return err
}
t = t.UTC()
after = t.Unix()
}
if hammerheadIntegration.Planned {
page = 0
totalPages = 0
stopped = false
for page <= totalPages && !stopped {
curTotalPages := totalPages
tours, curTotalPages, err := h.fetchTours(page)
if err != nil {
warning := fmt.Sprintf("error fetching tours from Hammerhead: %v\n", err)
fmt.Print(warning)
app.Logger().Warn(warning)
break
}
if curTotalPages > totalPages {
totalPages = curTotalPages
}
err, stopped = syncTrailWithTours(app, client, ctx, h, actor, hammerheadIntegration, tours, after)
if err != nil {
warning := fmt.Sprintf("error syncing Hammerhead tours with trails: %v\n", err)
fmt.Print(warning)
app.Logger().Warn(warning)
break
}
page += 1
}
}
if hammerheadIntegration.Completed {
page = 0
totalPages = 0
stopped = false
for page <= totalPages && !stopped {
curTotalPages := totalPages
tours, curTotalPages, err := h.fetchActivities(page)
if err != nil {
warning := fmt.Sprintf("error fetching tours from Hammerhead: %v\n", err)
fmt.Print(warning)
app.Logger().Warn(warning)
break
}
if curTotalPages > totalPages {
totalPages = curTotalPages
}
err, stopped = syncTrailWithActivities(app, client, ctx, h, actor, hammerheadIntegration, tours, after)
if err != nil {
warning := fmt.Sprintf("error syncing Hammerhead tours with trails: %v\n", err)
fmt.Print(warning)
app.Logger().Warn(warning)
break
}
page += 1
}
}
}
return nil
}
type BasicAuthToken struct {
Key string
Value string
}
func (b BasicAuthToken) Apply(req *http.Request) {
req.Header.Set("Authorization", "Bearer "+b.Value)
}
type HammerheadApi struct {
UserID string
Token string
}
func (h *HammerheadApi) buildHeader() *BasicAuthToken {
if h.UserID != "" && h.Token != "" {
return &BasicAuthToken{h.UserID, h.Token}
}
return nil
}
func getToken(uri string, auth *BasicAuthToken) ([]byte, error) {
client := &http.Client{}
var jsonStr = []byte(`{"grant_type": "password", "username": "` + auth.Key + `", "password": "` + auth.Value + `"}`)
req, err := http.NewRequest("POST", uri, bytes.NewBuffer(jsonStr))
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", "application/json")
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return nil, fmt.Errorf("error retrieving auth token from Hammerhead (%d): %s", resp.StatusCode, string(body))
}
return io.ReadAll(resp.Body)
}
func (h *HammerheadApi) UploadActivities(e *core.RequestEvent) error {
files, err := e.FindUploadedFiles("file")
if err != nil {
if errors.Is(err, http.ErrMissingFile) {
return apis.NewBadRequestError("file field is required", err)
}
return apis.NewBadRequestError("invalid multipart payload", err)
}
if len(files) == 0 {
return apis.NewBadRequestError("file field is required", nil)
}
fileToUpload := files[0]
reader, err := fileToUpload.Reader.Open()
if err != nil {
return err
}
defer reader.Close()
var buf bytes.Buffer
writer := multipart.NewWriter(&buf)
part, err := writer.CreateFormFile("file", fileToUpload.OriginalName)
if err != nil {
return err
}
if _, err := io.Copy(part, reader); err != nil {
return err
}
contentType := writer.FormDataContentType()
if err := writer.Close(); err != nil {
return err
}
currentURI := fmt.Sprintf("https://dashboard.hammerhead.io/v1/users/%s/routes/import/file", h.UserID)
if _, err := sendPostRequest(currentURI, &buf, contentType, h.buildHeader()); err != nil {
return err
}
return nil
}
func sendPostRequest(url string, body io.Reader, contentType string, auth *BasicAuthToken) ([]byte, error) {
client := &http.Client{}
req, err := http.NewRequest("POST", url, body)
if err != nil {
return nil, err
}
if contentType != "" {
req.Header.Set("Content-Type", contentType)
}
if auth != nil {
auth.Apply(req)
}
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return nil, fmt.Errorf("error sending request to Hammerhead (%d): %s", resp.StatusCode, string(body))
}
return io.ReadAll(resp.Body)
}
func sendGetRequest(url string, auth *BasicAuthToken) ([]byte, error) {
client := &http.Client{}
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return nil, err
}
if auth != nil {
auth.Apply(req)
}
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return nil, fmt.Errorf("error sending request to Hammerhead (%d): %s", resp.StatusCode, string(body))
}
return io.ReadAll(resp.Body)
}
func (h *HammerheadApi) Login(email, password string) error {
url := "https://dashboard.hammerhead.io/v1/auth/token"
body, err := getToken(url, &BasicAuthToken{email, password})
if err != nil {
return err
}
var data LoginResponse
json.Unmarshal(body, &data)
h.Token = data.Token
derivedUserID, err := extractUserIDFromToken(data.Token)
if err != nil {
return fmt.Errorf("unable to determine Hammerhead user id automatically: %w", err)
}
h.UserID = derivedUserID
return nil
}
func extractUserIDFromToken(token string) (string, error) {
parts := strings.Split(token, ".")
if len(parts) < 2 {
return "", errors.New("token is not a JWT")
}
payload, err := base64.RawURLEncoding.DecodeString(parts[1])
if err != nil {
return "", fmt.Errorf("unable to decode JWT payload: %w", err)
}
var claims map[string]any
if err := json.Unmarshal(payload, &claims); err != nil {
return "", fmt.Errorf("unable to decode JWT claims: %w", err)
}
if value, ok := claims["sub"].(string); ok && value != "" {
return value, nil
}
return "", errors.New("no sub claim found in token")
}
func (h *HammerheadApi) fetchActivities(page int) ([]HammerheadActivityResponse, int, error) {
currentUri := fmt.Sprintf("https://dashboard.hammerhead.io/v1/users/%s/activities?perPage=50&page=%d&search=&orderBy=NEWEST&ascending=true", h.UserID, page)
body, err := sendGetRequest(currentUri, h.buildHeader())
if err != nil {
return nil, 0, err
}
var data HammerheadActivitiesResponse
json.Unmarshal(body, &data)
tours := data.Tours
return tours, data.Pages, nil
}
func (h *HammerheadApi) fetchTours(page int) ([]HammerheadTourResponse, int, error) {
currentUri := fmt.Sprintf("https://dashboard.hammerhead.io/v1/users/%s/routes?perPage=50&page=%d&search=&orderBy=NEWEST&ascending=true&exclude=archive", h.UserID, page)
body, err := sendGetRequest(currentUri, h.buildHeader())
if err != nil {
return nil, 0, err
}
var data HammerheadToursResponse
json.Unmarshal(body, &data)
tours := data.Data
return tours, data.TotalPages, nil
}
func (h *HammerheadApi) fetchDetailedActivity(tour HammerheadActivityResponse) (*HammerheadActivity, error) {
url := fmt.Sprintf("https://dashboard.hammerhead.io/v1/users/%s/activities/%s/details", h.UserID, tour.ID)
body, err := sendGetRequest(url, h.buildHeader())
if err != nil {
return nil, err
}
var data *HammerheadActivity
json.Unmarshal(body, &data)
return data, nil
}
func (h *HammerheadApi) fetchDetailedTour(tour HammerheadTourResponse) (*HammerheadTour, error) {
url := fmt.Sprintf("https://dashboard.hammerhead.io/v1/users/%s/routes/%s", h.UserID, tour.ID)
body, err := sendGetRequest(url, h.buildHeader())
if err != nil {
return nil, err
}
var data *HammerheadTour
json.Unmarshal(body, &data)
return data, nil
}
func syncTrailWithTours(app core.App, client meilisearch.ServiceManager, ctx context.Context, k *HammerheadApi, actor *core.Record, integration HammerheadIntegration, tours []HammerheadTourResponse, after int64) (error, bool) {
for _, tour := range tours {
existingTrail, err := util.FindTrailByExternalReference(app, "hammerhead", tour.ID)
if err != nil {
return err, true
}
if existingTrail != nil {
continue
}
detailedTour, err := k.fetchDetailedTour(tour)
if err != nil {
app.Logger().Warn(fmt.Sprintf("Unable to fetch details for tour '%s': %v", tour.Name, err))
continue
}
if detailedTour.CreatedAt.Unix() < after {
return nil, true
}
if detailedTour.Distance <= 0 {
app.Logger().Warn(fmt.Sprintf("Skipping Hammerhead tour '%s' with zero distance", tour.Name))
continue
}
gpx, err := generateTourGPX(detailedTour)
if err != nil {
app.Logger().Warn(fmt.Sprintf("Unable to generate GPX for tour '%s': %v", tour.Name, err))
continue
}
trailID, err := createTrailFromTour(app, detailedTour, gpx, actor.Id)
if err != nil {
app.Logger().Warn(fmt.Sprintf("Unable to create trail for tour '%s': %v", tour.Name, err))
continue
}
if err := trailmerge.TryAutoMergeImportedTrail(app, client, ctx, actor, trailID, integration.Merge); err != nil {
app.Logger().Warn(fmt.Sprintf("Unable to auto-merge imported Hammerhead tour '%s': %v", tour.Name, err))
}
}
return nil, false
}
func syncTrailWithActivities(app core.App, client meilisearch.ServiceManager, ctx context.Context, k *HammerheadApi, actor *core.Record, integration HammerheadIntegration, tours []HammerheadActivityResponse, after int64) (error, bool) {
for _, tour := range tours {
existingTrail, err := util.FindTrailByExternalReference(app, "hammerhead", tour.ID)
if err != nil {
return err, true
}
if existingTrail != nil {
continue
}
detailedTour, err := k.fetchDetailedActivity(tour)
if err != nil {
app.Logger().Warn(fmt.Sprintf("Unable to fetch details for tour '%s': %v", tour.Name, err))
continue
}
if detailedTour.ActivityData.CreatedAt.Unix() < after {
return nil, true
}
distance, ok := activityDistance(detailedTour)
if !ok || distance <= 0 {
app.Logger().Warn(fmt.Sprintf("Skipping Hammerhead activity '%s' with zero distance", tour.Name))
continue
}
gpx, err := generateActivityGPX(detailedTour)
if err != nil {
app.Logger().Warn(fmt.Sprintf("Unable to generate GPX for tour '%s': %v", tour.Name, err))
continue
}
trailID, err := createTrailFromActivity(app, detailedTour, gpx, actor.Id)
if err != nil {
app.Logger().Warn(fmt.Sprintf("Unable to create trail for tour '%s': %v", tour.Name, err))
continue
}
if err := trailmerge.TryAutoMergeImportedTrail(app, client, ctx, actor, trailID, integration.Merge); err != nil {
app.Logger().Warn(fmt.Sprintf("Unable to auto-merge imported Hammerhead activity '%s': %v", tour.Name, err))
}
}
return nil, false
}
func activityDistance(detailedTour *HammerheadActivity) (float64, bool) {
idDistance := slices.IndexFunc(detailedTour.ActivityData.ActivityInfo, func(c HammerheadInfo) bool { return c.Key == "TYPE_DISTANCE_ID" })
if idDistance < 0 {
return 0, false
}
return detailedTour.ActivityData.ActivityInfo[idDistance].Value.Value, true
}
func createTrailFromActivity(app core.App, detailedTour *HammerheadActivity, gpx *filesystem.File, actor string) (string, error) {
trailid := security.RandomStringWithAlphabet(core.DefaultIdLength, core.DefaultIdAlphabet)
collection, err := app.FindCollectionByNameOrId("trails")
if err != nil {
return "", err
}
record := core.NewRecord(collection)
category, _ := app.FindFirstRecordByData("categories", "name", "Biking" /*ToDo: Mapping*/)
categoryId := ""
if category != nil {
categoryId = category.Id
}
diffculty := "easy" // ToDo: calculate difficulty
idDistance := slices.IndexFunc(detailedTour.ActivityData.ActivityInfo, func(c HammerheadInfo) bool { return c.Key == "TYPE_DISTANCE_ID" })
idElevationGain := slices.IndexFunc(detailedTour.ActivityData.ActivityInfo, func(c HammerheadInfo) bool { return c.Key == "TYPE_ELEVATION_GAIN_ID" })
idElevationLoss := slices.IndexFunc(detailedTour.ActivityData.ActivityInfo, func(c HammerheadInfo) bool { return c.Key == "TYPE_ELEVATION_LOSS_ID" })
duration := 0
for _, lap := range detailedTour.ActivityData.Laps {
duration += lap.ActiveTime
}
startLat := float64(0)
startLng := float64(0)
for i, lat := range detailedTour.RecordData.Lat {
if lat != float64(0) {
startLat = lat
startLng = detailedTour.RecordData.Lng[i]
break
}
}
record.Load(map[string]any{
"id": trailid,
"name": detailedTour.ActivityData.Name,
"public": false,
"completed": true,
"distance": detailedTour.ActivityData.ActivityInfo[idDistance].Value.Value,
"elevation_gain": detailedTour.ActivityData.ActivityInfo[idElevationGain].Value.Value,
"elevation_loss": detailedTour.ActivityData.ActivityInfo[idElevationLoss].Value.Value,
"duration": duration / 1000,
"date": detailedTour.ActivityData.CreatedAt,
"external_provider": "hammerhead",
"external_id": detailedTour.ActivityData.ID,
"lat": startLat,
"lon": startLng,
"difficulty": diffculty,
"category": categoryId,
"author": actor,
})
if gpx != nil {
record.Set("gpx", gpx)
}
if err := app.Save(record); err != nil {
return "", err
}
if err := util.EnsureTrailExternalReference(app, trailid, "hammerhead", detailedTour.ActivityData.ID); err != nil {
return "", err
}
collection, err = app.FindCollectionByNameOrId("summit_logs")
if err != nil {
return "", err
}
summitLogRecord := core.NewRecord(collection)
summitLogRecord.Load(map[string]any{
"distance": detailedTour.ActivityData.ActivityInfo[idDistance].Value.Value,
"elevation_gain": detailedTour.ActivityData.ActivityInfo[idElevationGain].Value.Value,
"elevation_loss": detailedTour.ActivityData.ActivityInfo[idElevationLoss].Value.Value,
"duration": duration / 1000,
"date": detailedTour.ActivityData.CreatedAt,
"author": actor,
"trail": trailid,
})
if err := app.Save(summitLogRecord); err != nil {
return "", err
}
return trailid, nil
}
func createTrailFromTour(app core.App, detailedTour *HammerheadTour, gpx *filesystem.File, actor string) (string, error) {
trailid := security.RandomStringWithAlphabet(core.DefaultIdLength, core.DefaultIdAlphabet)
collection, err := app.FindCollectionByNameOrId("trails")
if err != nil {
return "", err
}
record := core.NewRecord(collection)
category, _ := app.FindFirstRecordByData("categories", "name", "Biking" /*ToDo: Mapping*/)
categoryId := ""
if category != nil {
categoryId = category.Id
}
diffculty := "easy" // ToDo: calculate difficulty
record.Load(map[string]any{
"id": trailid,
"name": detailedTour.Name,
"public": detailedTour.IsPublic,
"distance": detailedTour.Distance,
"elevation_gain": detailedTour.Elevation.Gain,
"elevation_loss": detailedTour.Elevation.Loss,
"date": detailedTour.CreatedAt,
"lat": detailedTour.StartLocation.Lat,
"lon": detailedTour.StartLocation.Lng,
"difficulty": diffculty,
"category": categoryId,
"author": actor,
})
if gpx != nil {
record.Set("gpx", gpx)
}
if err := app.Save(record); err != nil {
return "", err
}
if err := util.EnsureTrailExternalReference(app, trailid, "hammerhead", detailedTour.ID); err != nil {
return "", err
}
return trailid, nil
}
func generateActivityGPX(detailedTour *HammerheadActivity) (*filesystem.File, error) {
times := len(detailedTour.RecordData.Timestamp)
if times == 0 {
return nil, nil
}
var points []gpx.GPXPoint
const zeroEps = 1e-4
// iterate over timestamps and only add points when lat/lng exist for the same index
for i := 0; i < times; i++ {
// ensure we have latitude and longitude for this index
if i < len(detailedTour.RecordData.Lat) && i < len(detailedTour.RecordData.Lng) {
lat := detailedTour.RecordData.Lat[i]
lng := detailedTour.RecordData.Lng[i]
// exclude near (0,0) garbage points
if math.Abs(lat) < zeroEps && math.Abs(lng) < zeroEps {
continue
}
t := detailedTour.RecordData.Timestamp[i]
elevation := float64(0)
if i < len(detailedTour.RecordData.Elevation) {
elevation = detailedTour.RecordData.Elevation[i] / 1000.0
}
points = append(points, gpx.GPXPoint{
Point: gpx.Point{
Latitude: lat,
Longitude: lng,
Elevation: *gpx.NewNullableFloat64(elevation),
},
Timestamp: time.Unix(int64(t), 0),
})
}
}
if len(points) == 0 {
return nil, nil
}
gpxData := &gpx.GPX{
Version: "1.1",
Creator: "Hammerhead GPX Exporter",
Tracks: []gpx.GPXTrack{
{
Name: detailedTour.ActivityData.Name,
Segments: []gpx.GPXTrackSegment{
{
Points: points,
},
},
},
},
}
gpxAsXML, err := gpxData.ToXml(gpx.ToXmlParams{Version: "1.1", Indent: true})
if err != nil {
return nil, err
}
gpxFile, err := filesystem.NewFileFromBytes(gpxAsXML, detailedTour.ActivityData.Name+".gpx")
if err != nil {
return nil, err
}
return gpxFile, nil
}
func generateTourGPX(detailedTour *HammerheadTour) (*filesystem.File, error) {
poly := detailedTour.RoutePolyline
coords, err := decodePolyline(poly)
if err != nil {
return nil, fmt.Errorf("decode polyline: %w", err)
}
if len(coords) == 0 {
return nil, nil
}
// try to get elevation polyline (adjust field path if your struct differs)
elevations := []float64{}
// precision 100 is common for Valhalla elevation encodings; change if needed
if decoded, err := decodeElevations(detailedTour.Elevation.Polyline, 100000); err == nil {
elevations = decoded
}
// Heuristic: detect if coords are (lng,lat) instead of (lat,lng).
// Count how many points look valid in each orientation and pick the best.
validAsLat := 0
validAsLng := 0
for _, c := range coords {
// treat c[0] as lat, c[1] as lng
if c[0] >= -90 && c[0] <= 90 && c[1] >= -180 && c[1] <= 180 {
validAsLat++
}
// treat c[1] as lat, c[0] as lng (swapped)
if c[1] >= -90 && c[1] <= 90 && c[0] >= -180 && c[0] <= 180 {
validAsLng++
}
}
swap := false
if validAsLng > validAsLat {
swap = true
}
var points []gpx.GPXPoint
for i, c := range coords {
lat := c[0]
lng := c[1]
if swap {
lat, lng = c[1], c[0]
}
// choose elevation:
elevation := 0.0
if len(elevations) == len(coords) {
elevation = elevations[i]
} else if len(elevations) > 0 {
// map index proportionally if lengths differ
j := int(math.Round(float64(i) * float64(len(elevations)-1) / float64(len(coords)-1)))
if j < 0 {
j = 0
}
if j >= len(elevations) {
j = len(elevations) - 1
}
elevation = elevations[j]
}
points = append(points, gpx.GPXPoint{
Point: gpx.Point{
Latitude: lat,
Longitude: lng,
Elevation: *gpx.NewNullableFloat64(elevation),
},
})
}
gpxData := &gpx.GPX{
Version: "1.1",
Creator: "Hammerhead GPX Exporter",
Tracks: []gpx.GPXTrack{
{
Name: detailedTour.Name,
Segments: []gpx.GPXTrackSegment{
{
Points: points,
},
},
},
},
}
gpxAsXML, err := gpxData.ToXml(gpx.ToXmlParams{Version: "1.1", Indent: true})
if err != nil {
return nil, err
}
gpxFile, err := filesystem.NewFileFromBytes(gpxAsXML, detailedTour.Name+".gpx")
if err != nil {
return nil, err
}
return gpxFile, nil
}
// decodePolyline decodes an encoded polyline string (Google Polyline Algorithm)
// returns slice of [lat, lng] pairs (precision 1e5).
func decodePolyline(s string) ([][2]float64, error) {
if s == "" {
return nil, nil
}
var coords [][2]float64
index := 0
lat := 0
lng := 0
for index < len(s) {
// decode latitude
result := 0
shift := uint(0)
for {
if index >= len(s) {
return nil, fmt.Errorf("invalid polyline encoding")
}
b := int(s[index]) - 63
index++
result |= (b & 0x1F) << shift
shift += 5
if b < 0x20 {
break
}
}
dlat := (result >> 1) ^ (-(result & 1))
lat += dlat
// decode longitude
result = 0
shift = 0
for {
if index >= len(s) {
return nil, fmt.Errorf("invalid polyline encoding")
}
b := int(s[index]) - 63
index++
result |= (b & 0x1F) << shift
shift += 5
if b < 0x20 {
break
}
}
dlng := (result >> 1) ^ (-(result & 1))
lng += dlng
coords = append(coords, [2]float64{float64(lat) / 1e5, float64(lng) / 1e5})
}
// Auto-normalize scale if values are out of realistic lat/lon ranges.
// Some providers use different precision/scales; repeatedly divide by 10
// until all values fit into valid ranges.
if len(coords) > 0 {
maxLat := 0.0
maxLng := 0.0
for _, c := range coords {
if abs := math.Abs(c[0]); abs > maxLat {
maxLat = abs
}
if abs := math.Abs(c[1]); abs > maxLng {
maxLng = abs
}
}
// If values are too large (e.g. > 90 lat or > 180 lon), rescale down.
for (maxLat > 90.0 || maxLng > 180.0) && (maxLat > 0 && maxLng > 0) {
for i := range coords {
coords[i][0] /= 10.0
coords[i][1] /= 10.0
}
maxLat /= 10.0
maxLng /= 10.0
}
}
return coords, nil
}
// decodeElevations decodes a single-dimension delta-encoded polyline string.
// precision is the divisor (e.g. 100 for centi-meters -> meters). Returns elevation values in same units as precision (meters if precision=100).
func decodeElevations(s string, precision float64) ([]float64, error) {
if s == "" {
return nil, nil
}
var elevs []float64
index := 0
val := 0
for index < len(s) {
result := 0
shift := uint(0)
for {
if index >= len(s) {
return nil, fmt.Errorf("invalid elevation encoding")
}
b := int(s[index]) - 63
index++
result |= (b & 0x1F) << shift
shift += 5
if b < 0x20 {
break
}
}
d := (result >> 1) ^ (-(result & 1))
val += d
elevs = append(elevs, float64(val)/precision)
}
return elevs, nil
}

View File

@@ -0,0 +1,209 @@
package hammerhead
import (
"time"
"pocketbase/services/trailmerge"
)
type HammerheadToursResponse struct {
TotalItems int `json:"totalItems"`
TotalPages int `json:"totalPages"`
PerPage int `json:"perPage"`
CurrentPage int `json:"currentPage"`
Data []HammerheadTourResponse `json:"data"`
}
type HammerheadTourResponse struct {
StartLocationName string `json:"startLocationName"`
IsAutoImported bool `json:"isAutoImported"`
SummaryPolyline string `json:"summaryPolyline"`
IsStarred bool `json:"isStarred"`
IsPublic bool `json:"isPublic"`
Collections any `json:"collections"`
Gain int `json:"gain"`
Distance float64 `json:"distance"`
Name string `json:"name"`
RoutingType string `json:"routingType"`
ID string `json:"id"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
Source string `json:"source"`
}
type HammerheadTourElevation struct {
Gain float64 `json:"gain"`
Loss float64 `json:"loss"`
Min float64 `json:"min"`
Max float64 `json:"max"`
Source string `json:"source"`
Polyline string `json:"polyline"`
}
type HammerheadLocation struct {
Lat float64 `json:"lat"`
Lng float64 `json:"lng"`
}
type HammerheadWaypoint struct {
Lat float64 `json:"lat"`
Lng float64 `json:"lng"`
WaypointType string `json:"waypointType"`
PolylineIndex int `json:"polylineIndex"`
}
type HammerheadTour struct {
ID string `json:"id"`
CreatedAt time.Time `json:"createdAt"`
Name string `json:"name"`
Distance float64 `json:"distance"`
Elevation HammerheadTourElevation `json:"elevation"`
IsStarred bool `json:"isStarred"`
StartLocationName string `json:"startLocationName"`
EndLocationName string `json:"endLocationName"`
StartLocation HammerheadLocation `json:"startLocation"`
EndLocation HammerheadLocation `json:"endLocation"`
Waypoints []HammerheadWaypoint `json:"waypoints"`
Collections []string `json:"collections"`
RoutePolyline string `json:"routePolyline"`
SummaryPolyline string `json:"summaryPolyline"`
Source string `json:"source"`
SourceID string `json:"sourceId"`
IsPublic bool `json:"isPublic"`
ImageVersion string `json:"imageVersion"`
IsAutoImported bool `json:"isAutoImported"`
UpdatedAt time.Time `json:"updatedAt"`
Bounds []HammerheadLocation `json:"bounds"`
}
type HammerheadIntegration struct {
Active bool `json:"active"`
Email string `json:"email"`
Password string `json:"password"`
Planned bool `json:"planned"`
Completed bool `json:"completed"`
After string `json:"after,omitempty"`
Merge trailmerge.IntegrationAutoMergeSettings `json:"merge"`
}
type LoginResponse struct {
Token string `json:"access_token"`
Type string `json:"token_type"`
Expires int `json:"expires_in"`
}
type HammerheadActivitiesResponse struct {
Items int `json:"totalItems"`
Pages int `json:"totalPages"`
PerPage int `json:"perPage"`
Tours []HammerheadActivityResponse `json:"data"`
}
type HammerheadActivityResponse struct {
ID string `json:"id"`
CreatedAt time.Time `json:"createdAt"`
Name string `json:"name"`
Client string `json:"client"`
ActiveTime int `json:"activeTime"`
Duration HammerheadTourDuration `json:"duration"`
Sync HammerheadSync `json:"partners"`
ActivityInfo []HammerheadInfo `json:"activityInfo"`
}
type HammerheadInfoValue struct {
Format string `json:"format"`
Value float64 `json:"value"`
}
type HammerheadInfo struct {
Key string `json:"key"`
Value HammerheadInfoValue `json:"value"`
}
type HammerheadPartner struct {
Partner string `json:"partner"`
NeedsUpload bool `json:"needsUpload"`
ExternalID string `json:"externalId"`
Attempts int `json:"attempts"`
UploadedAt time.Time `json:"uploadedAt"`
}
type HammerheadSync struct {
Description string `json:"description"`
Tags []any `json:"tags"`
Synced bool `json:"synced"`
Partners []HammerheadPartner `json:"partners"`
}
type HammerheadTourDuration struct {
ElapsedTime int `json:"elapsedTime"`
StartTime time.Time `json:"startTime"`
EndTime time.Time `json:"endTime"`
}
type HammerheadActivity struct {
ActivityData HammerheadActivityData `json:"activityData"`
SessionData HammerheadSessionData `json:"sessionData"`
RecordData HammerheadRecordData `json:"recordData"`
ShiftData HammerheadShiftData `json:"shiftData"`
LapData HammerheadLapData `json:"lapData"`
DeviceBatteryData HammerheadDeviceBatteryData `json:"deviceBatteryData"`
}
type HammerheadDuration struct {
ElapsedTime int `json:"elapsedTime"`
StartTime time.Time `json:"startTime"`
EndTime time.Time `json:"endTime"`
}
type HammerheadLapDetail struct {
ActiveTime int `json:"activeTime"`
Duration HammerheadDuration `json:"duration"`
LapNumber int `json:"lapNumber"`
Pauses []HammerheadDuration `json:"pauses"`
LapInfo []HammerheadInfo `json:"lapInfo"`
Trigger string `json:"trigger"`
}
type HammerheadActivityData struct {
ID string `json:"id"`
Name string `json:"name"`
BikeID string `json:"bikeId"`
Client string `json:"client"`
ActiveTime int `json:"activeTime"`
Duration HammerheadDuration `json:"duration"`
ActivityInfo []HammerheadInfo `json:"activityInfo"`
Laps []HammerheadLapDetail `json:"laps"`
Polyline string `json:"polyline"`
Sync HammerheadSync `json:"sync"`
ActivityType string `json:"activityType"`
Climbs []HammerheadClimb `json:"climbs"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
}
type HammerheadClimb struct {
StartDistance float64 `json:"startDistance"`
EndDistance float64 `json:"endDistance"`
Distance float64 `json:"distance"`
}
type HammerheadSessionData struct {
ThresholdPower int `json:"thresholdPower"`
FrontGears []int `json:"frontGears"`
RearGears []int `json:"rearGears"`
}
type HammerheadRecordData struct {
Distance []float64 `json:"distance"`
Timestamp []int `json:"timestamp"`
Elevation []float64 `json:"elevation"`
Grade []float64 `json:"grade"`
Lat []float64 `json:"lat"`
Lng []float64 `json:"lng"`
Speed []float64 `json:"speed"`
Power []any `json:"power"`
Temperature []int `json:"temperature"`
}
type HammerheadShiftData struct {
Timestamp []int `json:"timestamp"`
FrontChange []bool `json:"frontChange"`
FrontGear []int `json:"frontGear"`
RearGear []int `json:"rearGear"`
FrontGearNum []int `json:"frontGearNum"`
RearGearNum []int `json:"rearGearNum"`
}
type HammerheadLapData struct {
Timestamp []int `json:"timestamp"`
Trigger []string `json:"trigger"`
}
type HammerheadDeviceBatteryData struct {
Timestamp []int `json:"timestamp"`
DeviceBattery []int `json:"deviceBattery"`
}

View File

@@ -1,7 +1,7 @@
package komoot
import (
"bytes"
"context"
"encoding/base64"
"encoding/json"
"errors"
@@ -13,14 +13,18 @@ import (
"strings"
"time"
"github.com/meilisearch/meilisearch-go"
"github.com/pocketbase/dbx"
"github.com/pocketbase/pocketbase/core"
"github.com/pocketbase/pocketbase/tools/filesystem"
"github.com/pocketbase/pocketbase/tools/security"
"github.com/twpayne/go-gpx"
"github.com/tkrajina/gpxgo/gpx"
"pocketbase/services/trailmerge"
"pocketbase/util"
)
func SyncKomoot(app core.App) error {
func SyncKomoot(app core.App, client meilisearch.ServiceManager) error {
integrations, err := app.FindAllRecords("integrations", dbx.NewExp("true"))
if err != nil {
return err
@@ -40,12 +44,17 @@ func SyncKomoot(app core.App) error {
app.Logger().Warn(warning)
continue
}
actorId := actor.Id
ctx, err := util.GetSafeActorContext(nil, actor)
if err != nil {
continue
}
komootString := i.GetString("komoot")
komootIntegration := KomootIntegration{
Planned: true,
Completed: true,
Merge: trailmerge.DefaultIntegrationAutoMergeSettings(),
}
json.Unmarshal([]byte(komootString), &komootIntegration)
@@ -69,25 +78,27 @@ func SyncKomoot(app core.App) error {
app.Logger().Warn(warning)
continue
}
hasNewTours := true
page := 0
for hasNewTours {
tours, err := k.fetchTours(page)
totalPages := 1
for page := 0; page < totalPages; page++ {
tours, tp, err := k.fetchTours(page)
if err != nil {
warning := fmt.Sprintf("error fetching tours from komoot: %v\n", err)
warning := fmt.Sprintf("error fetching tours from komoot (page %d): %v\n", page, err)
fmt.Print(warning)
app.Logger().Warn(warning)
continue
break
}
totalPages = tp
hasNewTours, err = syncTrailWithTours(app, k, komootIntegration, userId, actorId, tours)
allAlreadySynced, err := syncTrailWithTours(app, client, ctx, k, komootIntegration, userId, actor, tours)
if err != nil {
warning := fmt.Sprintf("error syncing komoot tours with trails: %v\n", err)
fmt.Print(warning)
app.Logger().Warn(warning)
continue
break
}
if allAlreadySynced {
break
}
page += 1
}
}
@@ -157,20 +168,18 @@ func (k *KomootApi) Login(email, password string) error {
return nil
}
func (k *KomootApi) fetchTours(page int) ([]KomootTour, error) {
func (k *KomootApi) fetchTours(page int) ([]KomootTour, int, error) {
currentUri := fmt.Sprintf("https://api.komoot.de/v007/users/%s/tours/?page=%d&sort_field=date&sort_direction=desc&limit=30", k.UserID, page)
body, err := sendRequest(currentUri, k.buildHeader())
if err != nil {
return nil, err
return nil, 0, err
}
var data KomootToursResponse
json.Unmarshal(body, &data)
tours := data.Embedded.Tours
return tours, nil
return data.Embedded.Tours, data.Page.TotalPages, nil
}
func (k *KomootApi) fetchDetailedTour(tour KomootTour) (*DetailedKomootTour, error) {
@@ -185,17 +194,25 @@ func (k *KomootApi) fetchDetailedTour(tour KomootTour) (*DetailedKomootTour, err
return data, nil
}
func syncTrailWithTours(app core.App, k *KomootApi, i KomootIntegration, user string, actor string, tours []KomootTour) (bool, error) {
hasNewTours := false
// syncTrailWithTours imports tours not yet in the DB. Returns allAlreadySynced=true
// when every tour on this page was already imported, so the caller can stop paginating
// early during incremental syncs. Tours skipped due to type filters do NOT count as
// synced - only tours already present in the DB do.
func syncTrailWithTours(app core.App, client meilisearch.ServiceManager, ctx context.Context, k *KomootApi, i KomootIntegration, user string, actor *core.Record, tours []KomootTour) (bool, error) {
allAlreadySynced := true
for _, tour := range tours {
trails, err := app.FindRecordsByFilter("trails", "external_id = {:id}", "", 1, 0, dbx.Params{"id": strconv.Itoa(int(tour.ID))})
existingTrail, err := util.FindTrailByExternalReference(app, "komoot", strconv.Itoa(int(tour.ID)))
if err != nil {
return hasNewTours, err
return false, err
}
if len(trails) != 0 || (tour.Type == "tour_planned" && !i.Planned) || (tour.Type == "tour_recorded" && !i.Completed) {
if existingTrail != nil {
continue
}
// Tour is not yet in the DB - we must keep paginating regardless of type filter
allAlreadySynced = false
if (tour.Type == "tour_planned" && !i.Planned) || (tour.Type == "tour_recorded" && !i.Completed) {
continue
}
hasNewTours = true
detailedTour, err := k.fetchDetailedTour(tour)
if err != nil {
app.Logger().Warn(fmt.Sprintf("Unable to fetch details for tour '%s': %v", tour.Name, err))
@@ -206,27 +223,30 @@ func syncTrailWithTours(app core.App, k *KomootApi, i KomootIntegration, user st
app.Logger().Warn(fmt.Sprintf("Unable to generate GPX for tour '%s': %v", tour.Name, err))
continue
}
wpIds, err := createWaypointsFromTour(app, detailedTour, user)
if err != nil {
app.Logger().Warn(fmt.Sprintf("Unable to create waypoints for tour '%s': %v", tour.Name, err))
continue
}
err = createTrailFromTour(app, k, detailedTour, gpx, actor, wpIds)
trailid, err := createTrailFromTour(app, k, detailedTour, gpx, user, actor.Id, i.Privacy)
if err != nil {
app.Logger().Warn(fmt.Sprintf("Unable to create trail for tour '%s': %v", tour.Name, err))
continue
}
err = createWaypointsFromTour(app, detailedTour, user, trailid)
if err != nil {
app.Logger().Warn(fmt.Sprintf("Unable to create waypoints for tour '%s': %v", tour.Name, err))
continue
}
if err := trailmerge.TryAutoMergeImportedTrail(app, client, ctx, actor, trailid, i.Merge); err != nil {
app.Logger().Warn(fmt.Sprintf("Unable to auto-merge imported komoot tour '%s': %v", tour.Name, err))
}
}
return hasNewTours, nil
return allAlreadySynced, nil
}
func createTrailFromTour(app core.App, k *KomootApi, detailedTour *DetailedKomootTour, gpx *filesystem.File, actor string, wpIds []string) error {
func createTrailFromTour(app core.App, k *KomootApi, detailedTour *DetailedKomootTour, gpx *filesystem.File, user string, actor string, privacy string) (string, error) {
trailid := security.RandomStringWithAlphabet(core.DefaultIdLength, core.DefaultIdAlphabet)
collection, err := app.FindCollectionByNameOrId("trails")
if err != nil {
return err
return "", err
}
record := core.NewRecord(collection)
@@ -252,12 +272,12 @@ func createTrailFromTour(app core.App, k *KomootApi, detailedTour *DetailedKomoo
if len(detailedTour.Embedded.CoverImages.Embedded.Items) > 0 {
photos, err = fetchRoutePhotos(k, detailedTour)
if err != nil {
return err
return "", err
}
} else {
photo, err := fetchPhoto(detailedTour.MapImage.Src, "", "")
if err != nil {
return err
return "", err
}
photos = append(photos, photo)
}
@@ -267,10 +287,25 @@ func createTrailFromTour(app core.App, k *KomootApi, detailedTour *DetailedKomoo
diffculty = "easy"
}
public := detailedTour.Status == "public"
if privacy == "settings" {
privacySettings := struct {
Trails string `json:"trails"`
}{}
settings, _ := app.FindFirstRecordByData("settings", "user", user)
err = settings.UnmarshalJSONField("privacy", &privacySettings)
if err != nil {
return "", err
}
public = privacySettings.Trails == "public"
}
record.Load(map[string]any{
"id": trailid,
"name": detailedTour.Name,
"public": detailedTour.Status == "public",
"public": public,
"completed": detailedTour.Type == "tour_recorded",
"distance": detailedTour.Distance,
"elevation_gain": detailedTour.ElevationUp,
"elevation_loss": detailedTour.ElevationDown,
@@ -282,7 +317,6 @@ func createTrailFromTour(app core.App, k *KomootApi, detailedTour *DetailedKomoo
"lon": detailedTour.StartPoint.Lng,
"difficulty": diffculty,
"category": categoryId,
"waypoints": wpIds,
"author": actor,
})
@@ -294,13 +328,16 @@ func createTrailFromTour(app core.App, k *KomootApi, detailedTour *DetailedKomoo
}
if err := app.Save(record); err != nil {
return err
return "", err
}
if err := util.EnsureTrailExternalReference(app, trailid, "komoot", strconv.Itoa(detailedTour.ID)); err != nil {
return "", err
}
if detailedTour.Type == "tour_recorded" {
collection, err := app.FindCollectionByNameOrId("summit_logs")
if err != nil {
return err
return "", err
}
summitLogRecord := core.NewRecord(collection)
@@ -314,25 +351,23 @@ func createTrailFromTour(app core.App, k *KomootApi, detailedTour *DetailedKomoo
"trail": trailid,
})
if err := app.Save(summitLogRecord); err != nil {
return err
return "", err
}
}
return nil
return trailid, nil
}
func createWaypointsFromTour(app core.App, tour *DetailedKomootTour, user string) ([]string, error) {
func createWaypointsFromTour(app core.App, tour *DetailedKomootTour, user string, trailid string) error {
collection, err := app.FindCollectionByNameOrId("waypoints")
if err != nil {
return nil, err
return err
}
wpIds := make([]string, len(tour.Embedded.Timeline.Embedded.Items))
for i, wp := range tour.Embedded.Timeline.Embedded.Items {
for _, wp := range tour.Embedded.Timeline.Embedded.Items {
photos, err := fetchWaypointPhotos(wp)
if err != nil {
return nil, err
return err
}
record := core.NewRecord(collection)
@@ -359,6 +394,7 @@ func createWaypointsFromTour(app core.App, tour *DetailedKomootTour, user string
"icon": "circle",
"author": user,
"distance_from_start": 0,
"trail": trailid,
})
if photos != nil {
@@ -366,13 +402,11 @@ func createWaypointsFromTour(app core.App, tour *DetailedKomootTour, user string
}
if err := app.Save(record); err != nil {
return nil, err
return err
}
wpIds[i] = record.Id
}
return wpIds, nil
return nil
}
func fetchRoutePhotos(k *KomootApi, tour *DetailedKomootTour) ([]*filesystem.File, error) {
@@ -388,9 +422,9 @@ func fetchRoutePhotos(k *KomootApi, tour *DetailedKomootTour) ([]*filesystem.Fil
return nil, err
}
photos := make([]*filesystem.File, data.Page.TotalElements)
photos := make([]*filesystem.File, 0, len(data.Embedded.Items))
for i, img := range data.Embedded.Items {
for _, img := range data.Embedded.Items {
photo, err := fetchPhoto(img.Src, "", "")
if err != nil {
return nil, err
@@ -398,7 +432,7 @@ func fetchRoutePhotos(k *KomootApi, tour *DetailedKomootTour) ([]*filesystem.Fil
if strings.HasSuffix(photo.Name, ".gif") {
continue
}
photos[i] = photo
photos = append(photos, photo)
//TODO: komoot photos can have location data. Maybe we should create a waypoint for those photos?
}
@@ -408,9 +442,9 @@ func fetchRoutePhotos(k *KomootApi, tour *DetailedKomootTour) ([]*filesystem.Fil
func fetchWaypointPhotos(wp Item) ([]*filesystem.File, error) {
photos := make([]*filesystem.File, len(wp.Embedded.Reference.Embedded.Images.Embedded.Items))
photos := make([]*filesystem.File, 0, len(wp.Embedded.Reference.Embedded.Images.Embedded.Items))
for i, img := range wp.Embedded.Reference.Embedded.Images.Embedded.Items {
for _, img := range wp.Embedded.Reference.Embedded.Images.Embedded.Items {
photo, err := fetchPhoto(img.Src, "", "")
if err != nil {
return nil, err
@@ -418,7 +452,7 @@ func fetchWaypointPhotos(wp Item) ([]*filesystem.File, error) {
if strings.HasSuffix(photo.Name, ".gif") {
continue
}
photos[i] = photo
photos = append(photos, photo)
}
return photos, nil
@@ -438,35 +472,36 @@ func fetchPhoto(url string, width string, height string) (*filesystem.File, erro
}
func generateTourGPX(detailedTour *DetailedKomootTour) (*filesystem.File, error) {
var points []*gpx.WptType
var points []gpx.GPXPoint
for _, item := range detailedTour.Embedded.Coordinates.Items {
t := detailedTour.Date.Unix() + int64(item.T/1000)
points = append(points, &gpx.WptType{Lat: item.Lat, Lon: item.Lng, Ele: item.Alt, Time: time.Unix(t, 0)})
points = append(points, gpx.GPXPoint{
Point: gpx.Point{Latitude: item.Lat, Longitude: item.Lng, Elevation: *gpx.NewNullableFloat64(item.Alt)},
Timestamp: time.Unix(t, 0)})
}
gpx := &gpx.GPX{
gpxData := &gpx.GPX{
Version: "1.1",
Creator: "komoot GPX Exporter",
Trk: []*gpx.TrkType{
Tracks: []gpx.GPXTrack{
{
Name: detailedTour.Name,
TrkSeg: []*gpx.TrkSegType{
Segments: []gpx.GPXTrackSegment{
{
TrkPt: points,
Points: points,
},
},
},
},
}
var buf bytes.Buffer
err := gpx.Write(&buf)
gpxAsXML, err := gpxData.ToXml(gpx.ToXmlParams{Version: "1.1", Indent: true})
if err != nil {
return nil, err
}
gpxFile, err := filesystem.NewFileFromBytes(buf.Bytes(), detailedTour.Name+".gpx")
gpxFile, err := filesystem.NewFileFromBytes(gpxAsXML, detailedTour.Name+".gpx")
if err != nil {
return nil, err
}

View File

@@ -1,13 +1,19 @@
package komoot
import "time"
import (
"time"
"pocketbase/services/trailmerge"
)
type KomootIntegration struct {
Active bool `json:"active"`
Email string `json:"email"`
Password string `json:"password"`
Planned bool `json:"planned"`
Completed bool `json:"completed"`
Active bool `json:"active"`
Email string `json:"email"`
Password string `json:"password"`
Planned bool `json:"planned"`
Completed bool `json:"completed"`
Privacy string `json:"privacy"`
Merge trailmerge.IntegrationAutoMergeSettings `json:"merge"`
}
type LoginResponse struct {

View File

@@ -1,6 +1,10 @@
package strava
import "time"
import (
"time"
"pocketbase/services/trailmerge"
)
type TokenRequest struct {
ClientID int32 `json:"client_id"`
@@ -21,14 +25,17 @@ type RefreshTokenResponse struct {
ExpiresAt int64 `json:"expires_at"`
}
type StravaIntegration struct {
Active bool `json:"active"`
Routes bool `json:"routes"`
Activities bool `json:"activities"`
ClientID int32 `json:"clientId"`
ClientSecret string `json:"clientSecret"`
AccessToken string `json:"accessToken,omitempty"`
RefreshToken string `json:"refreshToken,omitempty"`
ExpiresAt int64 `json:"expiresAt,omitempty"`
Active bool `json:"active"`
Routes bool `json:"routes"`
Activities bool `json:"activities"`
ClientID int32 `json:"clientId"`
ClientSecret string `json:"clientSecret"`
AccessToken string `json:"accessToken,omitempty"`
RefreshToken string `json:"refreshToken,omitempty"`
ExpiresAt int64 `json:"expiresAt,omitempty"`
Privacy string `json:"privacy"`
After string `json:"after,omitempty"`
Merge trailmerge.IntegrationAutoMergeSettings `json:"merge"`
}
type StravaRoute struct {
Athlete Athlete `json:"athlete"`

View File

@@ -2,6 +2,7 @@ package strava
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
@@ -11,19 +12,23 @@ import (
"strconv"
"time"
"github.com/meilisearch/meilisearch-go"
"github.com/pocketbase/dbx"
"github.com/pocketbase/pocketbase/core"
"github.com/pocketbase/pocketbase/tools/filesystem"
"github.com/pocketbase/pocketbase/tools/security"
"github.com/twpayne/go-gpx"
"github.com/tkrajina/gpxgo/gpx"
"github.com/twpayne/go-polyline"
"pocketbase/services/trailmerge"
"pocketbase/util"
)
type StravaApi struct {
AceessToken string
}
func SyncStrava(app core.App) error {
func SyncStrava(app core.App, client meilisearch.ServiceManager) error {
integrations, err := app.FindAllRecords("integrations", dbx.NewExp("true"))
if err != nil {
return err
@@ -43,7 +48,11 @@ func SyncStrava(app core.App) error {
app.Logger().Warn(warning)
continue
}
actorId := actor.Id
ctx, err := util.GetSafeActorContext(nil, actor)
if err != nil {
continue
}
stravaString := i.GetString("strava")
var stravaIntegration StravaIntegration
@@ -89,22 +98,12 @@ func SyncStrava(app core.App) error {
stravaIntegration.ExpiresAt = r.ExpiresAt
}
b, err := json.Marshal(stravaIntegration)
if err != nil {
return err
}
i.Set("strava", string(b))
err = app.Save(i)
if err != nil {
return err
}
if stravaIntegration.Routes {
page := 1
hasNewRoutes := true
for hasNewRoutes {
hasMore := true
for hasMore {
routes, err := fetchStravaRoutes(r.AccessToken, page)
hasMore = len(routes) > 0
page += 1
if err != nil {
warning := fmt.Sprintf("error fetching routes from strava: %v\n", err)
@@ -112,7 +111,7 @@ func SyncStrava(app core.App) error {
app.Logger().Warn(warning)
break
}
hasNewRoutes, err = syncTrailsWithRoutes(app, r.AccessToken, userId, actorId, routes)
err = syncTrailsWithRoutes(app, client, ctx, stravaIntegration, r.AccessToken, userId, actor, routes)
if err != nil {
warning := fmt.Sprintf("error syncing strava routes with trails: %v\n", err)
fmt.Print(warning)
@@ -123,9 +122,20 @@ func SyncStrava(app core.App) error {
}
if stravaIntegration.Activities {
page := 1
hasNewActivities := true
for hasNewActivities {
activities, err := fetchStravaActivities(r.AccessToken, page)
hasMore := true
for hasMore {
var after int64 = 0
if stravaIntegration.After != "" {
t, err := time.Parse("2006-01-02", stravaIntegration.After)
if err != nil {
return err
}
t = t.UTC()
after = t.Unix()
}
activities, err := fetchStravaActivities(r.AccessToken, page, after)
hasMore = len(activities) > 0
page += 1
if err != nil {
warning := fmt.Sprintf("error fetching activities from strava: %v", err)
@@ -133,7 +143,8 @@ func SyncStrava(app core.App) error {
app.Logger().Warn(warning)
break
}
hasNewActivities, err = syncTrailsWithActivities(app, r.AccessToken, userId, actorId, activities)
err = syncTrailsWithActivities(app, client, ctx, stravaIntegration, r.AccessToken, userId, actor, activities)
if err != nil {
warning := fmt.Sprintf("error syncing strava activities with trails: %v", err)
fmt.Print(warning)
@@ -141,6 +152,17 @@ func SyncStrava(app core.App) error {
continue
}
}
}
b, err := json.Marshal(stravaIntegration)
if err != nil {
return err
}
i.Set("strava", string(b))
err = app.Save(i)
if err != nil {
return err
}
}
@@ -208,8 +230,8 @@ func fetchStravaRoutes(accessToken string, page int) ([]StravaRoute, error) {
return routes, nil
}
func fetchStravaActivities(accessToken string, page int) ([]StravaActivity, error) {
stravaRoutesURL := fmt.Sprintf("https://www.strava.com/api/v3/athlete/activities?page=%d", page)
func fetchStravaActivities(accessToken string, page int, after int64) ([]StravaActivity, error) {
stravaRoutesURL := fmt.Sprintf("https://www.strava.com/api/v3/athlete/activities?page=%d&after=%d", page, after)
req, err := http.NewRequest("GET", stravaRoutesURL, nil)
if err != nil {
return nil, err
@@ -235,36 +257,36 @@ func fetchStravaActivities(accessToken string, page int) ([]StravaActivity, erro
return activities, nil
}
func syncTrailsWithRoutes(app core.App, accessToken string, user string, actor string, routes []StravaRoute) (bool, error) {
hasNewRoutes := false
func syncTrailsWithRoutes(app core.App, client meilisearch.ServiceManager, ctx context.Context, i StravaIntegration, accessToken string, user string, actor *core.Record, routes []StravaRoute) error {
for _, route := range routes {
trails, err := app.FindRecordsByFilter("trails", "external_id = {:id}", "", 1, 0, dbx.Params{"id": route.IDStr})
existingTrail, err := util.FindTrailByExternalReference(app, "strava", route.IDStr)
if err != nil {
return hasNewRoutes, err
return err
}
if len(trails) != 0 {
if existingTrail != nil {
continue
}
hasNewRoutes = true
gpx, err := fetchRouteGPX(route, accessToken)
if err != nil {
app.Logger().Warn(fmt.Sprintf("Unable to fetch GPX for route '%s': %v", route.Name, err))
continue
}
wpIds, err := createWaypointsFromRoute(app, route, user)
if err != nil {
app.Logger().Warn(fmt.Sprintf("Unable to create waypoints for route '%s': %v", route.Name, err))
continue
}
err = createTrailFromRoute(app, route, gpx, actor, wpIds)
trailid, err := createTrailFromRoute(app, route, gpx, user, actor.Id, i.Privacy)
if err != nil {
app.Logger().Warn(fmt.Sprintf("Unable to create trail for route '%s': %v", route.Name, err))
continue
}
err = createWaypointsFromRoute(app, route, user, trailid)
if err != nil {
app.Logger().Warn(fmt.Sprintf("Unable to create waypoints for route '%s': %v", route.Name, err))
continue
}
if err := trailmerge.TryAutoMergeImportedTrail(app, client, ctx, actor, trailid, i.Merge); err != nil {
app.Logger().Warn(fmt.Sprintf("Unable to auto-merge imported Strava route '%s': %v", route.Name, err))
}
}
return hasNewRoutes, nil
return nil
}
func fetchRouteGPX(route StravaRoute, accessToken string) (*filesystem.File, error) {
@@ -305,10 +327,12 @@ func fetchRouteGPX(route StravaRoute, accessToken string) (*filesystem.File, err
return gpxFile, nil
}
func createTrailFromRoute(app core.App, route StravaRoute, gpx *filesystem.File, actor string, wpIds []string) error {
func createTrailFromRoute(app core.App, route StravaRoute, gpx *filesystem.File, user string, actor string, privacy string) (string, error) {
trailid := security.RandomStringWithAlphabet(core.DefaultIdLength, core.DefaultIdAlphabet)
collection, err := app.FindCollectionByNameOrId("trails")
if err != nil {
return err
return "", err
}
record := core.NewRecord(collection)
@@ -336,22 +360,36 @@ func createTrailFromRoute(app core.App, route StravaRoute, gpx *filesystem.File,
category = hikeCategory.Id
}
public := !route.Private
if privacy == "settings" {
privacySettings := struct {
Trails string `json:"trails"`
}{}
settings, _ := app.FindFirstRecordByData("settings", "user", user)
err = settings.UnmarshalJSONField("privacy", &privacySettings)
if err != nil {
return "", err
}
public = privacySettings.Trails == "public"
}
record.Load(map[string]any{
"name": route.Name,
"description": route.Description,
"public": !route.Private,
"distance": route.Distance,
"elevation_gain": route.ElevationGain,
"duration": route.EstimatedMovingTime,
"date": time.Unix(int64(route.Timestamp), 0),
"external_provider": "strava",
"external_id": route.IDStr,
"lat": lat,
"lon": lon,
"waypoints": wpIds,
"difficulty": "easy",
"category": category,
"author": actor,
"id": trailid,
"name": route.Name,
"description": route.Description,
"public": public,
"distance": route.Distance,
"elevation_gain": route.ElevationGain,
"duration": route.EstimatedMovingTime,
"date": time.Unix(int64(route.Timestamp), 0),
"lat": lat,
"lon": lon,
"difficulty": "easy",
"category": category,
"author": actor,
})
if gpx != nil {
@@ -359,20 +397,21 @@ func createTrailFromRoute(app core.App, route StravaRoute, gpx *filesystem.File,
}
if err := app.Save(record); err != nil {
return err
return "", err
}
if err := util.EnsureTrailExternalReference(app, trailid, "strava", route.IDStr); err != nil {
return "", err
}
return nil
return trailid, err
}
func createWaypointsFromRoute(app core.App, route StravaRoute, user string) ([]string, error) {
func createWaypointsFromRoute(app core.App, route StravaRoute, user string, trailid string) error {
collection, err := app.FindCollectionByNameOrId("waypoints")
if err != nil {
return nil, err
return err
}
wpIds := make([]string, len(route.Waypoints))
for i, wp := range route.Waypoints {
record := core.NewRecord(collection)
@@ -383,26 +422,26 @@ func createWaypointsFromRoute(app core.App, route StravaRoute, user string) ([]s
record.Set("icon", "circle")
record.Set("author", user)
record.Set("distance_from_start", wp.DistanceIntoRoute)
record.Set("trail", trailid)
app.Save(record)
if err := app.Save(record); err != nil {
return err
}
wpIds[i] = record.Id
}
return wpIds, nil
return nil
}
func syncTrailsWithActivities(app core.App, accessToken string, user string, actor string, activities []StravaActivity) (bool, error) {
hasNewActivites := false
func syncTrailsWithActivities(app core.App, client meilisearch.ServiceManager, ctx context.Context, i StravaIntegration, accessToken string, user string, actor *core.Record, activities []StravaActivity) error {
for _, activity := range activities {
trails, err := app.FindRecordsByFilter("trails", "external_id = {:id}", "", 1, 0, dbx.Params{"id": strconv.Itoa(int(activity.ID))})
existingTrail, err := util.FindTrailByExternalReference(app, "strava", strconv.Itoa(int(activity.ID)))
if err != nil {
return hasNewActivites, err
return err
}
if len(trails) != 0 {
if existingTrail != nil {
continue
}
hasNewActivites = true
detailedActivity, err := fetchDetailedActivity(activity, accessToken)
if err != nil {
app.Logger().Warn(fmt.Sprintf("Unable to fetch detailed activity '%s': %v", activity.Name, err))
@@ -413,15 +452,17 @@ func syncTrailsWithActivities(app core.App, accessToken string, user string, act
app.Logger().Warn(fmt.Sprintf("Unable to fetch GPX for activity '%s': %v", activity.Name, err))
continue
}
err = createTrailFromActivity(app, detailedActivity, gpx, actor)
trailID, err := createTrailFromActivity(app, detailedActivity, gpx, user, actor.Id, i.Privacy)
if err != nil {
app.Logger().Warn(fmt.Sprintf("Unable to create trail from activity '%s': %v", activity.Name, err))
continue
}
if err := trailmerge.TryAutoMergeImportedTrail(app, client, ctx, actor, trailID, i.Merge); err != nil {
app.Logger().Warn(fmt.Sprintf("Unable to auto-merge imported Strava activity '%s': %v", activity.Name, err))
}
}
return hasNewActivites, nil
return nil
}
func fetchDetailedActivity(activity StravaActivity, accessToken string) (*DetailedStravaActivity, error) {
@@ -451,21 +492,21 @@ func fetchDetailedActivity(activity StravaActivity, accessToken string) (*Detail
return &detailedActivity, nil
}
func createTrailFromActivity(app core.App, activity *DetailedStravaActivity, gpx *filesystem.File, user string) error {
func createTrailFromActivity(app core.App, activity *DetailedStravaActivity, gpx *filesystem.File, user string, actor string, privacy string) (string, error) {
if len(activity.StartLatlng) < 2 {
return nil
return "", nil
}
collection, err := app.FindCollectionByNameOrId("trails")
if err != nil {
return err
return "", err
}
var photo *filesystem.File
if len(activity.Photos.Primary.Urls.Num600) > 0 {
photo, err = fetchActivityPhoto(activity)
if err != nil {
return err
return "", err
}
}
@@ -517,21 +558,35 @@ func createTrailFromActivity(app core.App, activity *DetailedStravaActivity, gpx
categoryId = category.Id
}
public := !activity.Private
if privacy == "settings" {
privacySettings := struct {
Trails string `json:"trails"`
}{}
settings, _ := app.FindFirstRecordByData("settings", "user", user)
err = settings.UnmarshalJSONField("privacy", &privacySettings)
if err != nil {
return "", err
}
public = privacySettings.Trails == "public"
}
record.Load(map[string]any{
"name": activity.Name,
"description": activity.Description,
"public": !activity.Private,
"distance": activity.Distance,
"elevation_gain": activity.TotalElevationGain,
"duration": activity.ElapsedTime,
"date": activity.StartDate,
"external_provider": "strava",
"external_id": activity.ID,
"lat": activity.StartLatlng[0],
"lon": activity.StartLatlng[1],
"difficulty": "easy",
"category": categoryId,
"author": user,
"name": activity.Name,
"description": activity.Description,
"public": public,
"distance": activity.Distance,
"elevation_gain": activity.TotalElevationGain,
"duration": activity.ElapsedTime,
"date": activity.StartDate,
"lat": activity.StartLatlng[0],
"lon": activity.StartLatlng[1],
"difficulty": "easy",
"category": categoryId,
"author": actor,
})
if photo != nil {
@@ -543,10 +598,13 @@ func createTrailFromActivity(app core.App, activity *DetailedStravaActivity, gpx
}
if err := app.Save(record); err != nil {
return err
return "", err
}
if err := util.EnsureTrailExternalReference(app, record.Id, "strava", strconv.Itoa(int(activity.ID))); err != nil {
return "", err
}
return nil
return record.Id, nil
}
func fetchActivityPhoto(activity *DetailedStravaActivity) (*filesystem.File, error) {
@@ -610,7 +668,7 @@ func generateActivityGPX(activity *DetailedStravaActivity, accessToken string) (
timeStream := streamResponse.Time
altitudeStream := streamResponse.Altitude
var points []*gpx.WptType
var points []gpx.GPXPoint
for i, latlng := range latLngStream.Data {
lat := latlng[0]
@@ -618,30 +676,31 @@ func generateActivityGPX(activity *DetailedStravaActivity, accessToken string) (
alt := altitudeStream.Data[i]
t := activity.StartDate.Unix() + int64(timeStream.Data[i])
points = append(points, &gpx.WptType{Lat: lat, Lon: lon, Ele: alt, Time: time.Unix(t, 0)})
points = append(points, gpx.GPXPoint{
Point: gpx.Point{Latitude: lat, Longitude: lon, Elevation: *gpx.NewNullableFloat64(alt)},
Timestamp: time.Unix(t, 0)})
}
gpx := &gpx.GPX{
gpxData := &gpx.GPX{
Version: "1.1",
Creator: "Strava GPX Exporter",
Trk: []*gpx.TrkType{
Tracks: []gpx.GPXTrack{
{
Name: activity.Name,
TrkSeg: []*gpx.TrkSegType{
Segments: []gpx.GPXTrackSegment{
{
TrkPt: points,
Points: points,
},
},
},
},
}
var buf bytes.Buffer
err = gpx.Write(&buf)
gpxAsXML, err := gpxData.ToXml(gpx.ToXmlParams{Version: "1.1", Indent: true})
if err != nil {
return nil, err
}
gpxFile, err := filesystem.NewFileFromBytes(buf.Bytes(), activity.Name+".gpx")
gpxFile, err := filesystem.NewFileFromBytes(gpxAsXML, activity.Name+".gpx")
if err != nil {
return nil, err
}

1324
db/main.go

File diff suppressed because it is too large Load Diff

View File

@@ -28,7 +28,7 @@ func init() {
return err
}
_, err = client.Index("trails").UpdateFilterableAttributes(&[]string{
_, err = client.Index("trails").UpdateFilterableAttributes(&[]interface{}{
"_geo", "author", "category", "completed", "date", "difficulty", "distance", "elevation_gain", "elevation_loss", "public", "shares", "tags",
})
@@ -53,7 +53,7 @@ func init() {
return err
}
_, err = client.Index("lists").UpdateFilterableAttributes(&[]string{
_, err = client.Index("lists").UpdateFilterableAttributes(&[]interface{}{
"author", "public", "shares",
})
if err != nil {

View File

@@ -17,7 +17,7 @@ func init() {
// update collection data
if err := json.Unmarshal([]byte(`{
"indexes": [
"CREATE INDEX ` + "`" + `idx_6tD5RqfVk2` + "`" + ` ON ` + "`" + `trails` + "`" + ` (` + "`" + `iri` + "`" + `)"
"CREATE INDEX `+"`"+`idx_6tD5RqfVk2`+"`"+` ON `+"`"+`trails`+"`"+` (`+"`"+`iri`+"`"+`)"
]
}`), &collection); err != nil {
return err

View File

@@ -17,7 +17,7 @@ func init() {
// update collection data
if err := json.Unmarshal([]byte(`{
"indexes": [
"CREATE INDEX ` + "`" + `idx_hLtEU5XGWL` + "`" + ` ON ` + "`" + `lists` + "`" + ` (` + "`" + `iri` + "`" + `)"
"CREATE INDEX `+"`"+`idx_hLtEU5XGWL`+"`"+` ON `+"`"+`lists`+"`"+` (`+"`"+`iri`+"`"+`)"
]
}`), &collection); err != nil {
return err

View File

@@ -20,7 +20,7 @@ func init() {
return err
}
_, err = client.Index("trails").UpdateFilterableAttributes(&[]string{
_, err = client.Index("trails").UpdateFilterableAttributes(&[]any{
"_geo", "author", "category", "completed", "date", "difficulty", "distance", "elevation_gain", "elevation_loss", "public", "shares", "tags", "likes",
})
@@ -33,7 +33,7 @@ func init() {
return err
}
_, err = client.Index("trails").UpdateFilterableAttributes(&[]string{
_, err = client.Index("trails").UpdateFilterableAttributes(&[]any{
"_geo", "author", "category", "completed", "date", "difficulty", "distance", "elevation_gain", "elevation_loss", "public", "shares", "tags",
})

View File

@@ -17,7 +17,7 @@ func init() {
// update collection data
if err := json.Unmarshal([]byte(`{
"indexes": [
"CREATE UNIQUE INDEX ` + "`" + `idx_4T3m08OsP1` + "`" + ` ON ` + "`" + `comments` + "`" + ` (` + "`" + `iri` + "`" + `) WHERE iri IS NOT NULL AND iri != \"\";"
"CREATE UNIQUE INDEX `+"`"+`idx_4T3m08OsP1`+"`"+` ON `+"`"+`comments`+"`"+` (`+"`"+`iri`+"`"+`) WHERE iri IS NOT NULL AND iri != \"\";"
]
}`), &collection); err != nil {
return err

View File

@@ -17,7 +17,7 @@ func init() {
// update collection data
if err := json.Unmarshal([]byte(`{
"indexes": [
"CREATE UNIQUE INDEX ` + "`" + `idx_hLtEU5XGWL` + "`" + ` ON ` + "`" + `lists` + "`" + ` (` + "`" + `iri` + "`" + `) WHERE iri IS NOT NULL AND iri != \"\";"
"CREATE UNIQUE INDEX `+"`"+`idx_hLtEU5XGWL`+"`"+` ON `+"`"+`lists`+"`"+` (`+"`"+`iri`+"`"+`) WHERE iri IS NOT NULL AND iri != \"\";"
]
}`), &collection); err != nil {
return err
@@ -33,7 +33,7 @@ func init() {
// update collection data
if err := json.Unmarshal([]byte(`{
"indexes": [
"CREATE INDEX ` + "`" + `idx_hLtEU5XGWL` + "`" + ` ON ` + "`" + `lists` + "`" + ` (` + "`" + `iri` + "`" + `)"
"CREATE INDEX `+"`"+`idx_hLtEU5XGWL`+"`"+` ON `+"`"+`lists`+"`"+` (`+"`"+`iri`+"`"+`)"
]
}`), &collection); err != nil {
return err

View File

@@ -17,7 +17,7 @@ func init() {
// update collection data
if err := json.Unmarshal([]byte(`{
"indexes": [
"CREATE UNIQUE INDEX ` + "`" + `idx_iSbmEqYXbV` + "`" + ` ON ` + "`" + `summit_logs` + "`" + ` (` + "`" + `iri` + "`" + `) WHERE iri IS NOT NULL AND iri != \"\";"
"CREATE UNIQUE INDEX `+"`"+`idx_iSbmEqYXbV`+"`"+` ON `+"`"+`summit_logs`+"`"+` (`+"`"+`iri`+"`"+`) WHERE iri IS NOT NULL AND iri != \"\";"
]
}`), &collection); err != nil {
return err

View File

@@ -17,7 +17,7 @@ func init() {
// update collection data
if err := json.Unmarshal([]byte(`{
"indexes": [
"CREATE UNIQUE INDEX ` + "`" + `idx_6tD5RqfVk2` + "`" + ` ON ` + "`" + `trails` + "`" + ` (` + "`" + `iri` + "`" + `) WHERE iri IS NOT NULL AND iri != \"\";"
"CREATE UNIQUE INDEX `+"`"+`idx_6tD5RqfVk2`+"`"+` ON `+"`"+`trails`+"`"+` (`+"`"+`iri`+"`"+`) WHERE iri IS NOT NULL AND iri != \"\";"
]
}`), &collection); err != nil {
return err
@@ -33,7 +33,7 @@ func init() {
// update collection data
if err := json.Unmarshal([]byte(`{
"indexes": [
"CREATE INDEX ` + "`" + `idx_6tD5RqfVk2` + "`" + ` ON ` + "`" + `trails` + "`" + ` (` + "`" + `iri` + "`" + `)"
"CREATE INDEX `+"`"+`idx_6tD5RqfVk2`+"`"+` ON `+"`"+`trails`+"`"+` (`+"`"+`iri`+"`"+`)"
]
}`), &collection); err != nil {
return err

View File

@@ -0,0 +1,115 @@
package migrations
import (
"encoding/json"
"github.com/pocketbase/pocketbase/core"
m "github.com/pocketbase/pocketbase/migrations"
)
func init() {
m.Register(func(app core.App) error {
jsonData := `{
"createRule": null,
"deleteRule": null,
"fields": [
{
"autogeneratePattern": "[a-z0-9]{15}",
"hidden": false,
"id": "text3208210256",
"max": 15,
"min": 15,
"name": "id",
"pattern": "^[a-z0-9]+$",
"presentable": false,
"primaryKey": true,
"required": true,
"system": true,
"type": "text"
},
{
"cascadeDelete": true,
"collectionId": "pbc_1295301207",
"hidden": false,
"id": "relation1148540665",
"maxSelect": 1,
"minSelect": 0,
"name": "actor",
"presentable": false,
"required": true,
"system": false,
"type": "relation"
},
{
"cascadeDelete": true,
"collectionId": "pbc_1295301207",
"hidden": false,
"id": "relation3182418120",
"maxSelect": 1,
"minSelect": 0,
"name": "author",
"presentable": false,
"required": true,
"system": false,
"type": "relation"
},
{
"autogeneratePattern": "",
"hidden": false,
"id": "text521872670",
"max": 15,
"min": 15,
"name": "item",
"pattern": "^[a-z0-9]+$",
"presentable": false,
"primaryKey": false,
"required": true,
"system": false,
"type": "text"
},
{
"hidden": false,
"id": "autodate2990389176",
"name": "created",
"onCreate": true,
"onUpdate": false,
"presentable": false,
"system": false,
"type": "autodate"
},
{
"hidden": false,
"id": "autodate3332085495",
"name": "updated",
"onCreate": true,
"onUpdate": true,
"presentable": false,
"system": false,
"type": "autodate"
}
],
"id": "pbc_72164123",
"indexes": [],
"listRule": null,
"name": "feed",
"system": false,
"type": "base",
"updateRule": null,
"viewRule": null
}`
collection := &core.Collection{}
if err := json.Unmarshal([]byte(jsonData), &collection); err != nil {
return err
}
return app.Save(collection)
}, func(app core.App) error {
collection, err := app.FindCollectionByNameOrId("pbc_72164123")
if err != nil {
return err
}
return app.Delete(collection)
})
}

View File

@@ -0,0 +1,46 @@
package migrations
import (
"github.com/pocketbase/pocketbase/core"
m "github.com/pocketbase/pocketbase/migrations"
)
func init() {
m.Register(func(app core.App) error {
collection, err := app.FindCollectionByNameOrId("pbc_72164123")
if err != nil {
return err
}
// add field
if err := collection.Fields.AddMarshaledJSONAt(4, []byte(`{
"hidden": false,
"id": "select2363381545",
"maxSelect": 1,
"name": "type",
"presentable": false,
"required": true,
"system": false,
"type": "select",
"values": [
"trail",
"list",
"summit_log"
]
}`)); err != nil {
return err
}
return app.Save(collection)
}, func(app core.App) error {
collection, err := app.FindCollectionByNameOrId("pbc_72164123")
if err != nil {
return err
}
// remove field
collection.Fields.RemoveById("select2363381545")
return app.Save(collection)
})
}

View File

@@ -0,0 +1,40 @@
package migrations
import (
"encoding/json"
"github.com/pocketbase/pocketbase/core"
m "github.com/pocketbase/pocketbase/migrations"
)
func init() {
m.Register(func(app core.App) error {
collection, err := app.FindCollectionByNameOrId("pbc_72164123")
if err != nil {
return err
}
// update collection data
if err := json.Unmarshal([]byte(`{
"listRule": "actor.user = @request.auth.id"
}`), &collection); err != nil {
return err
}
return app.Save(collection)
}, func(app core.App) error {
collection, err := app.FindCollectionByNameOrId("pbc_72164123")
if err != nil {
return err
}
// update collection data
if err := json.Unmarshal([]byte(`{
"listRule": null
}`), &collection); err != nil {
return err
}
return app.Save(collection)
})
}

View File

@@ -0,0 +1,100 @@
package migrations
import (
"pocketbase/util"
"github.com/pocketbase/dbx"
"github.com/pocketbase/pocketbase/core"
m "github.com/pocketbase/pocketbase/migrations"
)
func init() {
m.Register(func(app core.App) error {
trails, err := app.FindAllRecords("trails")
if err != nil {
return err
}
for _, t := range trails {
trailAuthor, err := app.FindRecordById("activitypub_actors", t.GetString("author"))
if err != nil {
return err
}
feed, err := util.InsertIntoFeed(app, trailAuthor.Id, trailAuthor.Id, t.Id, util.TrailFeed)
if err != nil {
return err
}
feed.SetRaw("created", t.GetDateTime("created"))
err = app.Save(feed)
if err != nil {
return err
}
followers, err := app.FindRecordsByFilter("follows", "followee={:author}", "", -1, 0, dbx.Params{"author": trailAuthor.Id})
if err != nil {
return err
}
for _, f := range followers {
feed, err = util.InsertIntoFeed(app, f.GetString("follower"), trailAuthor.Id, t.Id, util.TrailFeed)
if err != nil {
return err
}
feed.SetRaw("created", t.GetDateTime("created"))
err = app.Save(feed)
if err != nil {
return err
}
}
}
lists, err := app.FindAllRecords("lists")
if err != nil {
return err
}
for _, t := range lists {
listAuthor, err := app.FindRecordById("activitypub_actors", t.GetString("author"))
if err != nil {
return err
}
feed, err := util.InsertIntoFeed(app, listAuthor.Id, listAuthor.Id, t.Id, util.ListFeed)
if err != nil {
return err
}
feed.SetRaw("created", t.GetDateTime("created"))
err = app.Save(feed)
if err != nil {
return err
}
followers, err := app.FindRecordsByFilter("follows", "followee={:author}", "", -1, 0, dbx.Params{"author": listAuthor.Id})
if err != nil {
return err
}
for _, f := range followers {
feed, err = util.InsertIntoFeed(app, f.GetString("follower"), listAuthor.Id, t.Id, util.ListFeed)
if err != nil {
return err
}
feed.SetRaw("created", t.GetDateTime("created"))
err = app.Save(feed)
if err != nil {
return err
}
}
}
return nil
}, func(app core.App) error {
_, err := app.DB().
NewQuery("DELETE FROM feed;").
Execute()
return err
})
}

View File

@@ -0,0 +1,106 @@
package migrations
import (
"encoding/json"
"github.com/pocketbase/pocketbase/core"
m "github.com/pocketbase/pocketbase/migrations"
)
func init() {
m.Register(func(app core.App) error {
jsonData := `{
"createRule": null,
"deleteRule": null,
"fields": [
{
"autogeneratePattern": "",
"hidden": false,
"id": "text3208210256",
"max": 0,
"min": 0,
"name": "id",
"pattern": "^[a-z0-9]+$",
"presentable": false,
"primaryKey": true,
"required": true,
"system": true,
"type": "text"
},
{
"hidden": false,
"id": "json1148540665",
"maxSize": 1,
"name": "actor",
"presentable": false,
"required": false,
"system": false,
"type": "json"
},
{
"hidden": false,
"id": "json3182418120",
"maxSize": 1,
"name": "author",
"presentable": false,
"required": false,
"system": false,
"type": "json"
},
{
"hidden": false,
"id": "json521872670",
"maxSize": 1,
"name": "item",
"presentable": false,
"required": false,
"system": false,
"type": "json"
},
{
"hidden": false,
"id": "json2363381545",
"maxSize": 1,
"name": "type",
"presentable": false,
"required": false,
"system": false,
"type": "json"
},
{
"hidden": false,
"id": "json2990389176",
"maxSize": 1,
"name": "created",
"presentable": false,
"required": false,
"system": false,
"type": "json"
}
],
"id": "pbc_1973704172",
"indexes": [],
"listRule": "",
"name": "profile_feed",
"system": false,
"type": "view",
"updateRule": null,
"viewQuery": "SELECT\n (ROW_NUMBER() OVER ()) as id,\n actor,\n author,\n item,\n type,\n created\nFROM\n (\n SELECT\n author as actor,\n author,\n id as item,\n \"list\" as type,\n created\n FROM\n lists\n UNION\n SELECT\n author as actor,\n author,\n id as item,\n \"trail\" as type,\n created\n FROM trails\n )\nORDER BY created desc;",
"viewRule": null
}`
collection := &core.Collection{}
if err := json.Unmarshal([]byte(jsonData), &collection); err != nil {
return err
}
return app.Save(collection)
}, func(app core.App) error {
collection, err := app.FindCollectionByNameOrId("pbc_1973704172")
if err != nil {
return err
}
return app.Delete(collection)
})
}

View File

@@ -0,0 +1,57 @@
package migrations
import (
"encoding/json"
"github.com/pocketbase/pocketbase/core"
m "github.com/pocketbase/pocketbase/migrations"
)
func init() {
m.Register(func(app core.App) error {
collection, err := app.FindCollectionByNameOrId("pbc_1973704172")
if err != nil {
return err
}
// update collection data
if err := json.Unmarshal([]byte(`{
"viewQuery": "SELECT\n (ROW_NUMBER() OVER ()) as id,\n actor,\n item,\n type,\n created\nFROM\n (\n SELECT\n author as actor,\n id as item,\n 'list' as type,\n created\n FROM\n lists\n UNION\n SELECT\n author as actor,\n id as item,\n 'trail' as type,\n created\n FROM trails\n )\nORDER BY created desc;"
}`), &collection); err != nil {
return err
}
// remove field
collection.Fields.RemoveById("json3182418120")
return app.Save(collection)
}, func(app core.App) error {
collection, err := app.FindCollectionByNameOrId("pbc_1973704172")
if err != nil {
return err
}
// update collection data
if err := json.Unmarshal([]byte(`{
"viewQuery": "SELECT\n (ROW_NUMBER() OVER ()) as id,\n actor,\n author,\n item,\n type,\n created\nFROM\n (\n SELECT\n author as actor,\n author,\n id as item,\n \"list\" as type,\n created\n FROM\n lists\n UNION\n SELECT\n author as actor,\n author,\n id as item,\n \"trail\" as type,\n created\n FROM trails\n )\nORDER BY created desc;"
}`), &collection); err != nil {
return err
}
// add field
if err := collection.Fields.AddMarshaledJSONAt(2, []byte(`{
"hidden": false,
"id": "json3182418120",
"maxSize": 1,
"name": "author",
"presentable": false,
"required": false,
"system": false,
"type": "json"
}`)); err != nil {
return err
}
return app.Save(collection)
})
}

View File

@@ -0,0 +1,44 @@
package migrations
import (
"github.com/pocketbase/pocketbase/core"
m "github.com/pocketbase/pocketbase/migrations"
)
func init() {
m.Register(func(app core.App) error {
collection, err := app.FindCollectionByNameOrId("pbc_72164123")
if err != nil {
return err
}
// remove field
collection.Fields.RemoveById("relation3182418120")
return app.Save(collection)
}, func(app core.App) error {
collection, err := app.FindCollectionByNameOrId("pbc_72164123")
if err != nil {
return err
}
// add field
if err := collection.Fields.AddMarshaledJSONAt(2, []byte(`{
"cascadeDelete": true,
"collectionId": "pbc_1295301207",
"hidden": false,
"id": "relation3182418120",
"maxSelect": 1,
"minSelect": 0,
"name": "author",
"presentable": false,
"required": true,
"system": false,
"type": "relation"
}`)); err != nil {
return err
}
return app.Save(collection)
})
}

View File

@@ -0,0 +1,216 @@
package migrations
import (
"encoding/json"
"github.com/pocketbase/pocketbase/core"
m "github.com/pocketbase/pocketbase/migrations"
)
func init() {
m.Register(func(app core.App) error {
collection, err := app.FindCollectionByNameOrId("pbc_468398817")
if err != nil {
return err
}
return app.Delete(collection)
}, func(app core.App) error {
jsonData := `{
"createRule": null,
"deleteRule": null,
"fields": [
{
"autogeneratePattern": "",
"hidden": false,
"id": "text3208210256",
"max": 0,
"min": 0,
"name": "id",
"pattern": "^[a-z0-9]+$",
"presentable": false,
"primaryKey": true,
"required": true,
"system": true,
"type": "text"
},
{
"hidden": false,
"id": "json2310347867",
"maxSize": 1,
"name": "trail_id",
"presentable": false,
"required": false,
"system": false,
"type": "json"
},
{
"hidden": false,
"id": "json3184124860",
"maxSize": 1,
"name": "trail_author_username",
"presentable": false,
"required": false,
"system": false,
"type": "json"
},
{
"hidden": false,
"id": "json2887874732",
"maxSize": 1,
"name": "trail_author_domain",
"presentable": false,
"required": false,
"system": false,
"type": "json"
},
{
"hidden": false,
"id": "json113557190",
"maxSize": 1,
"name": "trail_iri",
"presentable": false,
"required": false,
"system": false,
"type": "json"
},
{
"hidden": false,
"id": "json2862495610",
"maxSize": 1,
"name": "date",
"presentable": false,
"required": false,
"system": false,
"type": "json"
},
{
"hidden": false,
"id": "json1579384326",
"maxSize": 1,
"name": "name",
"presentable": false,
"required": false,
"system": false,
"type": "json"
},
{
"hidden": false,
"id": "json1843675174",
"maxSize": 1,
"name": "description",
"presentable": false,
"required": false,
"system": false,
"type": "json"
},
{
"hidden": false,
"id": "json3275261007",
"maxSize": 1,
"name": "gpx",
"presentable": false,
"required": false,
"system": false,
"type": "json"
},
{
"hidden": false,
"id": "json3182418120",
"maxSize": 1,
"name": "author",
"presentable": false,
"required": false,
"system": false,
"type": "json"
},
{
"hidden": false,
"id": "json142008537",
"maxSize": 1,
"name": "photos",
"presentable": false,
"required": false,
"system": false,
"type": "json"
},
{
"hidden": false,
"id": "json479369857",
"maxSize": 1,
"name": "distance",
"presentable": false,
"required": false,
"system": false,
"type": "json"
},
{
"hidden": false,
"id": "json2254405824",
"maxSize": 1,
"name": "duration",
"presentable": false,
"required": false,
"system": false,
"type": "json"
},
{
"hidden": false,
"id": "json3015100073",
"maxSize": 1,
"name": "elevation_gain",
"presentable": false,
"required": false,
"system": false,
"type": "json"
},
{
"hidden": false,
"id": "json3171089056",
"maxSize": 1,
"name": "elevation_loss",
"presentable": false,
"required": false,
"system": false,
"type": "json"
},
{
"hidden": false,
"id": "json2990389176",
"maxSize": 1,
"name": "created",
"presentable": false,
"required": false,
"system": false,
"type": "json"
},
{
"hidden": false,
"id": "json2363381545",
"maxSize": 1,
"name": "type",
"presentable": false,
"required": false,
"system": false,
"type": "json"
}
],
"id": "pbc_468398817",
"indexes": [],
"listRule": "@collection.trails.id ?= trail_id && @collection.trails.public ?= true",
"name": "timeline",
"system": false,
"type": "view",
"updateRule": null,
"viewQuery": "SELECT\n id,\n trail_id,\n trail_author_username,\n trail_author_domain,\n trail_iri,\n date,\n name,\n description,\n gpx,\n author,\n photos,\n distance,\n duration,\n elevation_gain,\n elevation_loss,\n created,\n type\nFROM\n (\n SELECT\n summit_logs.id,\n summit_logs.trail as trail_id,\n tapa.preferred_username as trail_author_username,\n tapa.domain as trail_author_domain,\n trails.iri as trail_iri,\n summit_logs.date,\n trails.name,\n text as description,\n summit_logs.gpx,\n sapa.iri as author,\n summit_logs.photos,\n summit_logs.distance,\n summit_logs.duration,\n summit_logs.elevation_gain,\n summit_logs.elevation_loss,\n summit_logs.created,\n \"summit_log\" as type\n FROM\n summit_logs\n JOIN trails ON summit_logs.trail = trails.id\n JOIN activitypub_actors sapa ON sapa.id = summit_logs.author\n JOIN activitypub_actors tapa ON tapa.id = trails.author\n UNION\n SELECT\n trails.id,\n trails.id as trail_id,\n activitypub_actors.preferred_username as trail_author_username,\n activitypub_actors.domain as trail_author_domain,\n trails.iri as trail_iri,\n date,\n trails.name,\n description,\n gpx,\n activitypub_actors.iri as author,\n photos,\n distance,\n duration,\n elevation_gain,\n elevation_loss,\n trails.created,\n \"trail\" as type\n FROM\n trails\n JOIN activitypub_actors ON activitypub_actors.id = trails.author\n )\nORDER BY\n created DESC;\n",
"viewRule": "@collection.trails.id ?= trail_id && @collection.trails.public ?= true"
}`
collection := &core.Collection{}
if err := json.Unmarshal([]byte(jsonData), &collection); err != nil {
return err
}
return app.Save(collection)
})
}

View File

@@ -0,0 +1,40 @@
package migrations
import (
"encoding/json"
"github.com/pocketbase/pocketbase/core"
m "github.com/pocketbase/pocketbase/migrations"
)
func init() {
m.Register(func(app core.App) error {
collection, err := app.FindCollectionByNameOrId("pbc_1973704172")
if err != nil {
return err
}
// update collection data
if err := json.Unmarshal([]byte(`{
"listRule": "actor.user.settings_via_user.privacy.account != 'private' || actor.user = @request.auth.id"
}`), &collection); err != nil {
return err
}
return app.Save(collection)
}, func(app core.App) error {
collection, err := app.FindCollectionByNameOrId("pbc_1973704172")
if err != nil {
return err
}
// update collection data
if err := json.Unmarshal([]byte(`{
"listRule": ""
}`), &collection); err != nil {
return err
}
return app.Save(collection)
})
}

View File

@@ -0,0 +1,40 @@
package migrations
import (
"encoding/json"
"github.com/pocketbase/pocketbase/core"
m "github.com/pocketbase/pocketbase/migrations"
)
func init() {
m.Register(func(app core.App) error {
collection, err := app.FindCollectionByNameOrId("pbc_1973704172")
if err != nil {
return err
}
// update collection data
if err := json.Unmarshal([]byte(`{
"viewQuery": "SELECT\n (ROW_NUMBER() OVER ()) as id,\n actor,\n item,\n type,\n created\nFROM\n (\n SELECT\n author as actor,\n id as item,\n 'list' as type,\n created\n FROM\n lists\n WHERE lists.public = TRUE\n UNION\n SELECT\n author as actor,\n id as item,\n 'trail' as type,\n created\n FROM trails\n WHERE trails.public = TRUE\n )\nORDER BY created desc;"
}`), &collection); err != nil {
return err
}
return app.Save(collection)
}, func(app core.App) error {
collection, err := app.FindCollectionByNameOrId("pbc_1973704172")
if err != nil {
return err
}
// update collection data
if err := json.Unmarshal([]byte(`{
"viewQuery": "SELECT\n (ROW_NUMBER() OVER ()) as id,\n actor,\n item,\n type,\n created\nFROM\n (\n SELECT\n author as actor,\n id as item,\n 'list' as type,\n created\n FROM\n lists\n UNION\n SELECT\n author as actor,\n id as item,\n 'trail' as type,\n created\n FROM trails\n )\nORDER BY created desc;"
}`), &collection); err != nil {
return err
}
return app.Save(collection)
})
}

View File

@@ -0,0 +1,42 @@
package migrations
import (
"encoding/json"
"github.com/pocketbase/pocketbase/core"
m "github.com/pocketbase/pocketbase/migrations"
)
func init() {
m.Register(func(app core.App) error {
collection, err := app.FindCollectionByNameOrId("pbc_1295301207")
if err != nil {
return err
}
// update collection data
if err := json.Unmarshal([]byte(`{
"listRule": "user = @request.auth.id",
"viewRule": "user = @request.auth.id"
}`), &collection); err != nil {
return err
}
return app.Save(collection)
}, func(app core.App) error {
collection, err := app.FindCollectionByNameOrId("pbc_1295301207")
if err != nil {
return err
}
// update collection data
if err := json.Unmarshal([]byte(`{
"listRule": "user.settings_via_user.privacy.account != 'private' || user = @request.auth.id",
"viewRule": "user.settings_via_user.privacy.account != 'private' || user = @request.auth.id"
}`), &collection); err != nil {
return err
}
return app.Save(collection)
})
}

View File

@@ -0,0 +1,116 @@
package migrations
import (
"encoding/json"
"github.com/pocketbase/pocketbase/core"
m "github.com/pocketbase/pocketbase/migrations"
)
func init() {
m.Register(func(app core.App) error {
jsonData := `{
"createRule": null,
"deleteRule": null,
"fields": [
{
"autogeneratePattern": "[a-z0-9]{15}",
"hidden": false,
"id": "text3208210256",
"max": 15,
"min": 15,
"name": "id",
"pattern": "^[a-z0-9]+$",
"presentable": false,
"primaryKey": true,
"required": true,
"system": true,
"type": "text"
},
{
"cascadeDelete": true,
"collectionId": "e864strfxo14pm4",
"hidden": false,
"id": "relation2993194383",
"maxSelect": 1,
"minSelect": 0,
"name": "trail",
"presentable": false,
"required": true,
"system": false,
"type": "relation"
},
{
"autogeneratePattern": "[a-z0-9]{32}",
"hidden": false,
"id": "text1597481275",
"max": 32,
"min": 32,
"name": "token",
"pattern": "",
"presentable": false,
"primaryKey": false,
"required": true,
"system": false,
"type": "text"
},
{
"hidden": false,
"id": "select3762918058",
"maxSelect": 1,
"name": "permission",
"presentable": false,
"required": true,
"system": false,
"type": "select",
"values": [
"view",
"edit"
]
},
{
"hidden": false,
"id": "autodate2990389176",
"name": "created",
"onCreate": true,
"onUpdate": false,
"presentable": false,
"system": false,
"type": "autodate"
},
{
"hidden": false,
"id": "autodate3332085495",
"name": "updated",
"onCreate": true,
"onUpdate": true,
"presentable": false,
"system": false,
"type": "autodate"
}
],
"id": "pbc_2865004035",
"indexes": [],
"listRule": null,
"name": "trail_link_share",
"system": false,
"type": "base",
"updateRule": null,
"viewRule": null
}`
collection := &core.Collection{}
if err := json.Unmarshal([]byte(jsonData), &collection); err != nil {
return err
}
return app.Save(collection)
}, func(app core.App) error {
collection, err := app.FindCollectionByNameOrId("pbc_2865004035")
if err != nil {
return err
}
return app.Delete(collection)
})
}

View File

@@ -0,0 +1,42 @@
package migrations
import (
"encoding/json"
"github.com/pocketbase/pocketbase/core"
m "github.com/pocketbase/pocketbase/migrations"
)
func init() {
m.Register(func(app core.App) error {
collection, err := app.FindCollectionByNameOrId("pbc_2865004035")
if err != nil {
return err
}
// update collection data
if err := json.Unmarshal([]byte(`{
"indexes": [
"CREATE UNIQUE INDEX `+"`"+`idx_YbuWvsh5la`+"`"+` ON `+"`"+`trail_link_share`+"`"+` (`+"`"+`trail`+"`"+`)"
]
}`), &collection); err != nil {
return err
}
return app.Save(collection)
}, func(app core.App) error {
collection, err := app.FindCollectionByNameOrId("pbc_2865004035")
if err != nil {
return err
}
// update collection data
if err := json.Unmarshal([]byte(`{
"indexes": []
}`), &collection); err != nil {
return err
}
return app.Save(collection)
})
}

View File

@@ -0,0 +1,48 @@
package migrations
import (
"encoding/json"
"github.com/pocketbase/pocketbase/core"
m "github.com/pocketbase/pocketbase/migrations"
)
func init() {
m.Register(func(app core.App) error {
collection, err := app.FindCollectionByNameOrId("pbc_2865004035")
if err != nil {
return err
}
// update collection data
if err := json.Unmarshal([]byte(`{
"createRule": "trail.author.user = @request.auth.id",
"deleteRule": "trail.author.user = @request.auth.id",
"listRule": "trail.author.user = @request.auth.id",
"updateRule": "trail.author.user = @request.auth.id",
"viewRule": "trail.author.user = @request.auth.id"
}`), &collection); err != nil {
return err
}
return app.Save(collection)
}, func(app core.App) error {
collection, err := app.FindCollectionByNameOrId("pbc_2865004035")
if err != nil {
return err
}
// update collection data
if err := json.Unmarshal([]byte(`{
"createRule": null,
"deleteRule": null,
"listRule": null,
"updateRule": null,
"viewRule": null
}`), &collection); err != nil {
return err
}
return app.Save(collection)
})
}

View File

@@ -0,0 +1,42 @@
package migrations
import (
"encoding/json"
"github.com/pocketbase/pocketbase/core"
m "github.com/pocketbase/pocketbase/migrations"
)
func init() {
m.Register(func(app core.App) error {
collection, err := app.FindCollectionByNameOrId("e864strfxo14pm4")
if err != nil {
return err
}
// update collection data
if err := json.Unmarshal([]byte(`{
"listRule": "author.user = @request.auth.id || public = true || (@request.auth.id != \"\" && trail_share_via_trail.actor.user ?= @request.auth.id) || trail_link_share_via_trail.token = @request.query.share",
"viewRule": "author.user = @request.auth.id || public = true || (@request.auth.id != \"\" && trail_share_via_trail.actor.user ?= @request.auth.id) || trail_link_share_via_trail.token = @request.query.share "
}`), &collection); err != nil {
return err
}
return app.Save(collection)
}, func(app core.App) error {
collection, err := app.FindCollectionByNameOrId("e864strfxo14pm4")
if err != nil {
return err
}
// update collection data
if err := json.Unmarshal([]byte(`{
"listRule": "author.user = @request.auth.id || public = true || (@request.auth.id != \"\" && trail_share_via_trail.actor.user ?= @request.auth.id)",
"viewRule": "author.user = @request.auth.id || public = true || (@request.auth.id != \"\" && trail_share_via_trail.actor.user ?= @request.auth.id)"
}`), &collection); err != nil {
return err
}
return app.Save(collection)
})
}

View File

@@ -0,0 +1,42 @@
package migrations
import (
"encoding/json"
"github.com/pocketbase/pocketbase/core"
m "github.com/pocketbase/pocketbase/migrations"
)
func init() {
m.Register(func(app core.App) error {
collection, err := app.FindCollectionByNameOrId("goeo2ubp103rzp9")
if err != nil {
return err
}
// update collection data
if err := json.Unmarshal([]byte(`{
"listRule": "author = @request.auth.id || trails_via_waypoints.author.user ?= @request.auth.id || trails_via_waypoints.public ?= true || \n(@collection.trail_share.trail.id ?= trails_via_waypoints.id && @collection.trail_share.actor.user ?= @request.auth.id)\n|| \n(@collection.trail_link_share.trail.id ?= trails_via_waypoints.id && @collection.trail_link_share.token = @request.query.share)",
"viewRule": "author = @request.auth.id || trails_via_waypoints.author.user ?= @request.auth.id || trails_via_waypoints.public ?= true || \n(@collection.trail_share.trail.id ?= trails_via_waypoints.id && @collection.trail_share.actor.user ?= @request.auth.id)\n|| \n(@collection.trail_link_share.trail.id ?= trails_via_waypoints.id && @collection.trail_link_share.token = @request.query.share)"
}`), &collection); err != nil {
return err
}
return app.Save(collection)
}, func(app core.App) error {
collection, err := app.FindCollectionByNameOrId("goeo2ubp103rzp9")
if err != nil {
return err
}
// update collection data
if err := json.Unmarshal([]byte(`{
"listRule": "author = @request.auth.id || trails_via_waypoints.author.user ?= @request.auth.id || trails_via_waypoints.public ?= true || \n(@collection.trail_share.trail.id ?= trails_via_waypoints.id && @collection.trail_share.actor.user ?= @request.auth.id)",
"viewRule": "author = @request.auth.id || trails_via_waypoints.author.user ?= @request.auth.id || trails_via_waypoints.public ?= true || \n(@collection.trail_share.trail.id ?= trails_via_waypoints.id && @collection.trail_share.actor.user ?= @request.auth.id)"
}`), &collection); err != nil {
return err
}
return app.Save(collection)
})
}

View File

@@ -0,0 +1,42 @@
package migrations
import (
"encoding/json"
"github.com/pocketbase/pocketbase/core"
m "github.com/pocketbase/pocketbase/migrations"
)
func init() {
m.Register(func(app core.App) error {
collection, err := app.FindCollectionByNameOrId("pbc_1295301207")
if err != nil {
return err
}
// update collection data
if err := json.Unmarshal([]byte(`{
"listRule": "",
"viewRule": ""
}`), &collection); err != nil {
return err
}
return app.Save(collection)
}, func(app core.App) error {
collection, err := app.FindCollectionByNameOrId("pbc_1295301207")
if err != nil {
return err
}
// update collection data
if err := json.Unmarshal([]byte(`{
"listRule": "user = @request.auth.id",
"viewRule": "user = @request.auth.id"
}`), &collection); err != nil {
return err
}
return app.Save(collection)
})
}

View File

@@ -0,0 +1,78 @@
package migrations
import (
"github.com/pocketbase/pocketbase/core"
m "github.com/pocketbase/pocketbase/migrations"
)
func init() {
m.Register(func(app core.App) error {
collection, err := app.FindCollectionByNameOrId("uavt73rsqcn1n13")
if err != nil {
return err
}
// update field
if err := collection.Fields.AddMarshaledJSONAt(1, []byte(`{
"hidden": false,
"id": "0sepzvkh",
"maxSelect": 1,
"name": "language",
"presentable": false,
"required": false,
"system": false,
"type": "select",
"values": [
"en",
"de",
"fr",
"hu",
"it",
"nl",
"pl",
"pt",
"zh",
"es",
"eu",
"ru"
]
}`)); err != nil {
return err
}
return app.Save(collection)
}, func(app core.App) error {
collection, err := app.FindCollectionByNameOrId("uavt73rsqcn1n13")
if err != nil {
return err
}
// update field
if err := collection.Fields.AddMarshaledJSONAt(1, []byte(`{
"hidden": false,
"id": "0sepzvkh",
"maxSelect": 1,
"name": "language",
"presentable": false,
"required": false,
"system": false,
"type": "select",
"values": [
"en",
"de",
"fr",
"hu",
"it",
"nl",
"pl",
"pt",
"zh",
"es"
]
}`)); err != nil {
return err
}
return app.Save(collection)
})
}

View File

@@ -0,0 +1,42 @@
package migrations
import (
"encoding/json"
"github.com/pocketbase/pocketbase/core"
m "github.com/pocketbase/pocketbase/migrations"
)
func init() {
m.Register(func(app core.App) error {
collection, err := app.FindCollectionByNameOrId("e864strfxo14pm4")
if err != nil {
return err
}
// update collection data
if err := json.Unmarshal([]byte(`{
"listRule": "author.user = @request.auth.id || public = true || (@request.auth.id != \"\" && trail_share_via_trail.actor.user ?= @request.auth.id) || (trail_link_share_via_trail.token != \"\" && trail_link_share_via_trail.token = @request.query.share)",
"viewRule": "author.user = @request.auth.id || public = true || (@request.auth.id != \"\" && trail_share_via_trail.actor.user ?= @request.auth.id) || (trail_link_share_via_trail.token != \"\" && trail_link_share_via_trail.token = @request.query.share)"
}`), &collection); err != nil {
return err
}
return app.Save(collection)
}, func(app core.App) error {
collection, err := app.FindCollectionByNameOrId("e864strfxo14pm4")
if err != nil {
return err
}
// update collection data
if err := json.Unmarshal([]byte(`{
"listRule": "author.user = @request.auth.id || public = true || (@request.auth.id != \"\" && trail_share_via_trail.actor.user ?= @request.auth.id) || trail_link_share_via_trail.token = @request.query.share",
"viewRule": "author.user = @request.auth.id || public = true || (@request.auth.id != \"\" && trail_share_via_trail.actor.user ?= @request.auth.id) || trail_link_share_via_trail.token = @request.query.share "
}`), &collection); err != nil {
return err
}
return app.Save(collection)
})
}

View File

@@ -0,0 +1,42 @@
package migrations
import (
"encoding/json"
"github.com/pocketbase/pocketbase/core"
m "github.com/pocketbase/pocketbase/migrations"
)
func init() {
m.Register(func(app core.App) error {
collection, err := app.FindCollectionByNameOrId("goeo2ubp103rzp9")
if err != nil {
return err
}
// update collection data
if err := json.Unmarshal([]byte(`{
"listRule": "author = @request.auth.id || trails_via_waypoints.author.user ?= @request.auth.id || trails_via_waypoints.public ?= true || \n(@collection.trail_share.trail.id ?= trails_via_waypoints.id && @collection.trail_share.actor.user ?= @request.auth.id)\n|| \n(@collection.trail_link_share.token != \"\" && @collection.trail_link_share.trail.waypoints.id ?= id)",
"viewRule": "author = @request.auth.id || trails_via_waypoints.author.user ?= @request.auth.id || trails_via_waypoints.public ?= true || \n(@collection.trail_share.trail.id ?= trails_via_waypoints.id && @collection.trail_share.actor.user ?= @request.auth.id)\n|| \n(@collection.trail_link_share.token != \"\" && @collection.trail_link_share.trail.waypoints.id ?= id)"
}`), &collection); err != nil {
return err
}
return app.Save(collection)
}, func(app core.App) error {
collection, err := app.FindCollectionByNameOrId("goeo2ubp103rzp9")
if err != nil {
return err
}
// update collection data
if err := json.Unmarshal([]byte(`{
"listRule": "author = @request.auth.id || trails_via_waypoints.author.user ?= @request.auth.id || trails_via_waypoints.public ?= true || \n(@collection.trail_share.trail.id ?= trails_via_waypoints.id && @collection.trail_share.actor.user ?= @request.auth.id)\n|| \n(@collection.trail_link_share.trail.id ?= trails_via_waypoints.id && @collection.trail_link_share.token = @request.query.share)",
"viewRule": "author = @request.auth.id || trails_via_waypoints.author.user ?= @request.auth.id || trails_via_waypoints.public ?= true || \n(@collection.trail_share.trail.id ?= trails_via_waypoints.id && @collection.trail_share.actor.user ?= @request.auth.id)\n|| \n(@collection.trail_link_share.trail.id ?= trails_via_waypoints.id && @collection.trail_link_share.token = @request.query.share)"
}`), &collection); err != nil {
return err
}
return app.Save(collection)
})
}

View File

@@ -0,0 +1,62 @@
package migrations
import (
"encoding/json"
"github.com/pocketbase/pocketbase/core"
m "github.com/pocketbase/pocketbase/migrations"
)
func init() {
m.Register(func(app core.App) error {
collection, err := app.FindCollectionByNameOrId("goeo2ubp103rzp9")
if err != nil {
return err
}
// update collection data
if err := json.Unmarshal([]byte(`{
"listRule": "author = @request.auth.id || trail.author.user ?= @request.auth.id || trail.public ?= true || trail.trail_share_via_trail.actor.user ?= @request.auth.id\n|| \n(trail.trail_link_share_via_trail.token != \"\" && trail.trail_link_share_via_trail.token = @request.query.share)",
"viewRule": "author = @request.auth.id || trail.author.user ?= @request.auth.id || trail.public ?= true || trail.trail_share_via_trail.actor.user ?= @request.auth.id\n|| \n(trail.trail_link_share_via_trail.token != \"\" && trail.trail_link_share_via_trail.token = @request.query.share)"
}`), &collection); err != nil {
return err
}
// add field
if err := collection.Fields.AddMarshaledJSONAt(9, []byte(`{
"cascadeDelete": true,
"collectionId": "e864strfxo14pm4",
"hidden": false,
"id": "relation2993194383",
"maxSelect": 1,
"minSelect": 0,
"name": "trail",
"presentable": false,
"required": true,
"system": false,
"type": "relation"
}`)); err != nil {
return err
}
return app.Save(collection)
}, func(app core.App) error {
collection, err := app.FindCollectionByNameOrId("goeo2ubp103rzp9")
if err != nil {
return err
}
// update collection data
if err := json.Unmarshal([]byte(`{
"listRule": "author = @request.auth.id || trails_via_waypoints.author.user ?= @request.auth.id || trails_via_waypoints.public ?= true || \n(@collection.trail_share.trail.id ?= trails_via_waypoints.id && @collection.trail_share.actor.user ?= @request.auth.id)\n|| \n(@collection.trail_link_share.token != \"\" && @collection.trail_link_share.trail.waypoints.id ?= id)",
"viewRule": "author = @request.auth.id || trails_via_waypoints.author.user ?= @request.auth.id || trails_via_waypoints.public ?= true || \n(@collection.trail_share.trail.id ?= trails_via_waypoints.id && @collection.trail_share.actor.user ?= @request.auth.id)\n|| \n(@collection.trail_link_share.token != \"\" && @collection.trail_link_share.trail.waypoints.id ?= id)"
}`), &collection); err != nil {
return err
}
// remove field
collection.Fields.RemoveById("relation2993194383")
return app.Save(collection)
})
}

View File

@@ -0,0 +1,34 @@
package migrations
import (
"github.com/pocketbase/dbx"
"github.com/pocketbase/pocketbase/core"
m "github.com/pocketbase/pocketbase/migrations"
)
func init() {
m.Register(func(app core.App) error {
wps, err := app.FindAllRecords("waypoints")
if err != nil {
return err
}
for _, wp := range wps {
trail, err := app.FindFirstRecordByFilter("trails", "waypoints ?~ {:id}", dbx.Params{"id": wp.Id})
if err != nil {
continue
}
wp.Set("trail", trail.Id)
err = app.UnsafeWithoutHooks().Save(wp)
if err != nil {
return err
}
}
return nil
}, func(app core.App) error {
// add down queries...
return nil
})
}

View File

@@ -0,0 +1,44 @@
package migrations
import (
"github.com/pocketbase/pocketbase/core"
m "github.com/pocketbase/pocketbase/migrations"
)
func init() {
m.Register(func(app core.App) error {
collection, err := app.FindCollectionByNameOrId("e864strfxo14pm4")
if err != nil {
return err
}
// remove field
collection.Fields.RemoveById("ppq2sist")
return app.Save(collection)
}, func(app core.App) error {
collection, err := app.FindCollectionByNameOrId("e864strfxo14pm4")
if err != nil {
return err
}
// add field
if err := collection.Fields.AddMarshaledJSONAt(16, []byte(`{
"cascadeDelete": false,
"collectionId": "goeo2ubp103rzp9",
"hidden": false,
"id": "ppq2sist",
"maxSelect": 2147483647,
"minSelect": 0,
"name": "waypoints",
"presentable": false,
"required": false,
"system": false,
"type": "relation"
}`)); err != nil {
return err
}
return app.Save(collection)
})
}

View File

@@ -0,0 +1,46 @@
package migrations
import (
"encoding/json"
"github.com/pocketbase/pocketbase/core"
m "github.com/pocketbase/pocketbase/migrations"
)
func init() {
m.Register(func(app core.App) error {
collection, err := app.FindCollectionByNameOrId("pbc_1295301207")
if err != nil {
return err
}
// update collection data
if err := json.Unmarshal([]byte(`{
"indexes": [
"CREATE UNIQUE INDEX `+"`"+`idx_rpT7QJwWTm`+"`"+` ON `+"`"+`activitypub_actors`+"`"+` (`+"`"+`iri`+"`"+`)",
"CREATE INDEX idx_actors_username_domain\nON activitypub_actors(preferred_username, domain);",
"CREATE INDEX idx_activitypub_actors_user ON activitypub_actors(user);"
]
}`), &collection); err != nil {
return err
}
return app.Save(collection)
}, func(app core.App) error {
collection, err := app.FindCollectionByNameOrId("pbc_1295301207")
if err != nil {
return err
}
// update collection data
if err := json.Unmarshal([]byte(`{
"indexes": [
"CREATE UNIQUE INDEX `+"`"+`idx_rpT7QJwWTm`+"`"+` ON `+"`"+`activitypub_actors`+"`"+` (`+"`"+`iri`+"`"+`)"
]
}`), &collection); err != nil {
return err
}
return app.Save(collection)
})
}

View File

@@ -0,0 +1,42 @@
package migrations
import (
"encoding/json"
"github.com/pocketbase/pocketbase/core"
m "github.com/pocketbase/pocketbase/migrations"
)
func init() {
m.Register(func(app core.App) error {
collection, err := app.FindCollectionByNameOrId("goeo2ubp103rzp9")
if err != nil {
return err
}
// update collection data
if err := json.Unmarshal([]byte(`{
"deleteRule": "@request.auth.id != \"\" && (trail.author.user = @request.auth.id || author = @request.auth.id)",
"updateRule": "@request.auth.id != \"\" && (trail.author.user = @request.auth.id || author = @request.auth.id)"
}`), &collection); err != nil {
return err
}
return app.Save(collection)
}, func(app core.App) error {
collection, err := app.FindCollectionByNameOrId("goeo2ubp103rzp9")
if err != nil {
return err
}
// update collection data
if err := json.Unmarshal([]byte(`{
"deleteRule": "@request.auth.id != \"\" && ((@collection.trails.waypoints.id ?= id && @collection.trails.author.user = @request.auth.id) || author = @request.auth.id)",
"updateRule": "@request.auth.id != \"\" && ((@collection.trails.waypoints.id ?= id && @collection.trails.author.user = @request.auth.id) || author = @request.auth.id)"
}`), &collection); err != nil {
return err
}
return app.Save(collection)
})
}

View File

@@ -0,0 +1,41 @@
package migrations
import (
"github.com/pocketbase/pocketbase/core"
m "github.com/pocketbase/pocketbase/migrations"
)
func init() {
m.Register(func(app core.App) error {
collection, err := app.FindCollectionByNameOrId("uavt73rsqcn1n13")
if err != nil {
return err
}
// add field
if err := collection.Fields.AddMarshaledJSONAt(12, []byte(`{
"hidden": false,
"id": "json1001103536",
"maxSize": 0,
"name": "behavior",
"presentable": false,
"required": false,
"system": false,
"type": "json"
}`)); err != nil {
return err
}
return app.Save(collection)
}, func(app core.App) error {
collection, err := app.FindCollectionByNameOrId("uavt73rsqcn1n13")
if err != nil {
return err
}
// remove field
collection.Fields.RemoveById("json1001103536")
return app.Save(collection)
})
}

View File

@@ -0,0 +1,41 @@
package migrations
import (
"github.com/pocketbase/pocketbase/core"
m "github.com/pocketbase/pocketbase/migrations"
)
func init() {
m.Register(func(app core.App) error {
collection, err := app.FindCollectionByNameOrId("iz4sezoehde64wp")
if err != nil {
return err
}
// add field
if err := collection.Fields.AddMarshaledJSONAt(4, []byte(`{
"hidden": false,
"id": "json2528191900",
"maxSize": 2000000,
"name": "hammerhead",
"presentable": false,
"required": false,
"system": false,
"type": "json"
}`)); err != nil {
return err
}
return app.Save(collection)
}, func(app core.App) error {
collection, err := app.FindCollectionByNameOrId("iz4sezoehde64wp")
if err != nil {
return err
}
// remove field
collection.Fields.RemoveById("json2528191900")
return app.Save(collection)
})
}

View File

@@ -0,0 +1,61 @@
package migrations
import (
"github.com/pocketbase/pocketbase/core"
m "github.com/pocketbase/pocketbase/migrations"
)
func init() {
m.Register(func(app core.App) error {
collection, err := app.FindCollectionByNameOrId("e864strfxo14pm4")
if err != nil {
return err
}
// update field
if err := collection.Fields.AddMarshaledJSONAt(20, []byte(`{
"hidden": false,
"id": "htr35nha",
"maxSelect": 1,
"name": "external_provider",
"presentable": false,
"required": false,
"system": false,
"type": "select",
"values": [
"strava",
"komoot",
"hammerhead"
]
}`)); err != nil {
return err
}
return app.Save(collection)
}, func(app core.App) error {
collection, err := app.FindCollectionByNameOrId("e864strfxo14pm4")
if err != nil {
return err
}
// update field
if err := collection.Fields.AddMarshaledJSONAt(20, []byte(`{
"hidden": false,
"id": "htr35nha",
"maxSelect": 1,
"name": "external_provider",
"presentable": false,
"required": false,
"system": false,
"type": "select",
"values": [
"strava",
"komoot"
]
}`)); err != nil {
return err
}
return app.Save(collection)
})
}

View File

@@ -0,0 +1,60 @@
package migrations
import (
"github.com/pocketbase/pocketbase/core"
m "github.com/pocketbase/pocketbase/migrations"
)
func init() {
m.Register(func(app core.App) error {
collection, err := app.FindCollectionByNameOrId("kjxvi8asj2igqwf")
if err != nil {
return err
}
// add field
if err := collection.Fields.AddMarshaledJSONAt(6, []byte(`{
"hidden": false,
"id": "json3846545605",
"maxSize": 0,
"name": "settings",
"presentable": false,
"required": false,
"system": false,
"type": "json"
}`)); err != nil {
return err
}
if err := app.Save(collection); err != nil {
return err
}
records, err := app.FindAllRecords("categories")
if err != nil {
return err
}
for _, record := range records {
record.Set("settings", map[string]any{
"wp_merge_enabled": true,
"wp_merge_radius": 50,
})
if err := app.Save(record); err != nil {
return err
}
}
return nil
}, func(app core.App) error {
collection, err := app.FindCollectionByNameOrId("kjxvi8asj2igqwf")
if err != nil {
return err
}
// remove field
collection.Fields.RemoveById("json3846545605")
return app.Save(collection)
})
}

View File

@@ -0,0 +1,38 @@
package migrations
import (
"encoding/json"
"github.com/pocketbase/pocketbase/core"
m "github.com/pocketbase/pocketbase/migrations"
)
func init() {
m.Register(func(app core.App) error {
collection, err := app.FindCollectionByNameOrId("urytyc428mwlbqq")
if err != nil {
return err
}
if err := json.Unmarshal([]byte(`{
"viewQuery": "SELECT \n a.id, a.user, \n COALESCE(MAX(t.max_lat), 0) AS max_lat, \n COALESCE(MAX(t.max_lon), 0) AS max_lon, \n COALESCE(MIN(t.min_lat), 0) AS min_lat, \n COALESCE(MIN(t.min_lon), 0) AS min_lon \nFROM activitypub_actors a \nLEFT JOIN ( \n SELECT author AS actor_id, \n MAX(lat) AS max_lat, \n MAX(lon) AS max_lon, \n MIN(lat) AS min_lat, \n MIN(lon) AS min_lon \n FROM trails \n GROUP BY author \n UNION ALL \n SELECT ts.actor AS actor_id, \n MAX(t.lat) AS max_lat, \n MAX(t.lon) AS max_lon, \n MIN(t.lat) AS min_lat, \n MIN(t.lon) AS min_lon \n FROM trail_share ts \n JOIN trails t ON t.id = ts.trail \n GROUP BY ts.actor \n UNION ALL \n SELECT a2.id AS actor_id, \n p.max_lat, p.max_lon, p.min_lat, p.min_lon \n FROM activitypub_actors a2 \n CROSS JOIN ( \n SELECT \n MAX(lat) AS max_lat, \n MAX(lon) AS max_lon, \n MIN(lat) AS min_lat, \n MIN(lon) AS min_lon \n FROM trails \n WHERE public = TRUE \n ) p \n) t ON t.actor_id = a.id \nGROUP BY a.id;"
}`), &collection); err != nil {
return err
}
return app.Save(collection)
}, func(app core.App) error {
collection, err := app.FindCollectionByNameOrId("urytyc428mwlbqq")
if err != nil {
return err
}
if err := json.Unmarshal([]byte(`{
"viewQuery": "SELECT \n activitypub_actors.id, activitypub_actors.user, \n COALESCE(MAX(trails.lat), 0) AS max_lat, \n COALESCE(MAX(trails.lon), 0) AS max_lon, \n COALESCE(MIN(trails.lat), 0) AS min_lat, \n COALESCE(MIN(trails.lon), 0) AS min_lon \nFROM activitypub_actors \nLEFT JOIN trails \n ON activitypub_actors.id = trails.author \n OR trails.public = TRUE \n OR EXISTS (\n SELECT 1 \n FROM trail_share \n WHERE trail_share.trail = trails.id \n AND trail_share.actor = activitypub_actors.id\n ) \nGROUP BY activitypub_actors.id;"
}`), &collection); err != nil {
return err
}
return app.Save(collection)
})
}

View File

@@ -0,0 +1,120 @@
package migrations
import (
"encoding/json"
"github.com/pocketbase/pocketbase/core"
m "github.com/pocketbase/pocketbase/migrations"
)
func init() {
m.Register(func(app core.App) error {
collection, err := app.FindCollectionByNameOrId("trails_filter")
if err != nil {
return err
}
viewQuery := `SELECT
a.id,
a.user,
COALESCE(printf("%.2f", MAX(t.max_distance)), 0) AS max_distance,
COALESCE(printf("%.2f", MAX(t.max_elevation_gain)), 0) AS max_elevation_gain,
COALESCE(printf("%.2f", MAX(t.max_elevation_loss)), 0) AS max_elevation_loss,
COALESCE(printf("%.2f", MAX(t.max_duration)), 0) AS max_duration,
COALESCE(printf("%.2f", MIN(t.min_distance)), 0) AS min_distance,
COALESCE(printf("%.2f", MIN(t.min_elevation_gain)), 0) AS min_elevation_gain,
COALESCE(printf("%.2f", MIN(t.min_elevation_loss)), 0) AS min_elevation_loss,
COALESCE(printf("%.2f", MIN(t.min_duration)), 0) AS min_duration
FROM activitypub_actors a
LEFT JOIN (
SELECT author AS actor_id,
MAX(distance) AS max_distance,
MIN(distance) AS min_distance,
MAX(elevation_gain) AS max_elevation_gain,
MIN(elevation_gain) AS min_elevation_gain,
MAX(elevation_loss) AS max_elevation_loss,
MIN(elevation_loss) AS min_elevation_loss,
MAX(duration) AS max_duration,
MIN(duration) AS min_duration
FROM trails
GROUP BY author
UNION ALL
SELECT ts.actor AS actor_id,
MAX(t.distance) AS max_distance,
MIN(t.distance) AS min_distance,
MAX(t.elevation_gain) AS max_elevation_gain,
MIN(t.elevation_gain) AS min_elevation_gain,
MAX(t.elevation_loss) AS max_elevation_loss,
MIN(t.elevation_loss) AS min_elevation_loss,
MAX(t.duration) AS max_duration,
MIN(t.duration) AS min_duration
FROM trail_share ts
JOIN trails t ON t.id = ts.trail
GROUP BY ts.actor
UNION ALL
SELECT a2.id AS actor_id,
p.max_distance,
p.min_distance,
p.max_elevation_gain,
p.min_elevation_gain,
p.max_elevation_loss,
p.min_elevation_loss,
p.max_duration,
p.min_duration
FROM activitypub_actors a2
CROSS JOIN (
SELECT
MAX(distance) AS max_distance,
MIN(distance) AS min_distance,
MAX(elevation_gain) AS max_elevation_gain,
MIN(elevation_gain) AS min_elevation_gain,
MAX(elevation_loss) AS max_elevation_loss,
MIN(elevation_loss) AS min_elevation_loss,
MAX(duration) AS max_duration,
MIN(duration) AS min_duration
FROM trails
WHERE public = TRUE
) p
) t ON t.actor_id = a.id
GROUP BY a.id;`
if err := json.Unmarshal([]byte(`{"viewQuery": `+jsonMarshal(viewQuery)+`}`), &collection); err != nil {
return err
}
return app.Save(collection)
}, func(app core.App) error {
collection, err := app.FindCollectionByNameOrId("trails_filter")
if err != nil {
return err
}
viewQuery := `SELECT activitypub_actors.id, activitypub_actors.user, COALESCE(printf("%.2f", MAX(trails.distance)), 0) AS max_distance,
COALESCE(printf("%.2f", MAX(trails.elevation_gain)), 0) AS max_elevation_gain,
COALESCE(printf("%.2f", MAX(trails.elevation_loss)), 0) AS max_elevation_loss,
COALESCE(printf("%.2f", MAX(trails.duration)), 0) AS max_duration,
COALESCE(printf("%.2f", MIN(trails.distance)), 0) AS min_distance,
COALESCE(printf("%.2f", MIN(trails.elevation_gain)), 0) AS min_elevation_gain,
COALESCE(printf("%.2f", MIN(trails.elevation_loss)), 0) AS min_elevation_loss,
COALESCE(printf("%.2f", MIN(trails.duration)), 0) AS min_duration
FROM activitypub_actors
LEFT JOIN trails ON
activitypub_actors.id = trails.author OR
trails.public = 1 OR
EXISTS (
SELECT 1
FROM trail_share
WHERE trail_share.trail = trails.id
AND trail_share.actor = activitypub_actors.id
) GROUP BY activitypub_actors.id;`
if err := json.Unmarshal([]byte(`{"viewQuery": `+jsonMarshal(viewQuery)+`}`), &collection); err != nil {
return err
}
return app.Save(collection)
})
}
func jsonMarshal(value string) string {
b, _ := json.Marshal(value)
return string(b)
}

View File

@@ -0,0 +1,81 @@
package migrations
import (
"github.com/pocketbase/pocketbase/core"
m "github.com/pocketbase/pocketbase/migrations"
)
func init() {
m.Register(func(app core.App) error {
collection, err := app.FindCollectionByNameOrId("uavt73rsqcn1n13")
if err != nil {
return err
}
// update field
if err := collection.Fields.AddMarshaledJSONAt(1, []byte(`{
"hidden": false,
"id": "0sepzvkh",
"maxSelect": 1,
"name": "language",
"presentable": false,
"required": false,
"system": false,
"type": "select",
"values": [
"en",
"de",
"fr",
"hu",
"it",
"nl",
"pl",
"pt",
"zh",
"es",
"eu",
"ru",
"no"
]
}`)); err != nil {
return err
}
return app.Save(collection)
}, func(app core.App) error {
collection, err := app.FindCollectionByNameOrId("uavt73rsqcn1n13")
if err != nil {
return err
}
// update field
if err := collection.Fields.AddMarshaledJSONAt(1, []byte(`{
"hidden": false,
"id": "0sepzvkh",
"maxSelect": 1,
"name": "language",
"presentable": false,
"required": false,
"system": false,
"type": "select",
"values": [
"en",
"de",
"fr",
"hu",
"it",
"nl",
"pl",
"pt",
"zh",
"es",
"eu",
"ru"
]
}`)); err != nil {
return err
}
return app.Save(collection)
})
}

View File

@@ -0,0 +1,83 @@
package migrations
import (
"github.com/pocketbase/pocketbase/core"
m "github.com/pocketbase/pocketbase/migrations"
)
func init() {
m.Register(func(app core.App) error {
collection, err := app.FindCollectionByNameOrId("uavt73rsqcn1n13")
if err != nil {
return err
}
// update field
if err := collection.Fields.AddMarshaledJSONAt(1, []byte(`{
"hidden": false,
"id": "0sepzvkh",
"maxSelect": 1,
"name": "language",
"presentable": false,
"required": false,
"system": false,
"type": "select",
"values": [
"en",
"de",
"fr",
"hu",
"it",
"nl",
"pl",
"pt",
"zh",
"es",
"eu",
"ru",
"no",
"cs"
]
}`)); err != nil {
return err
}
return app.Save(collection)
}, func(app core.App) error {
collection, err := app.FindCollectionByNameOrId("uavt73rsqcn1n13")
if err != nil {
return err
}
// update field
if err := collection.Fields.AddMarshaledJSONAt(1, []byte(`{
"hidden": false,
"id": "0sepzvkh",
"maxSelect": 1,
"name": "language",
"presentable": false,
"required": false,
"system": false,
"type": "select",
"values": [
"en",
"de",
"fr",
"hu",
"it",
"nl",
"pl",
"pt",
"zh",
"es",
"eu",
"ru",
"no"
]
}`)); err != nil {
return err
}
return app.Save(collection)
})
}

View File

@@ -0,0 +1,45 @@
package migrations
import (
"github.com/pocketbase/pocketbase/core"
m "github.com/pocketbase/pocketbase/migrations"
)
func init() {
m.Register(func(app core.App) error {
collection, err := app.FindCollectionByNameOrId("_pb_users_auth_")
if err != nil {
return err
}
// remove field
collection.Fields.RemoveById("dlzhxcn2")
return app.Save(collection)
}, func(app core.App) error {
collection, err := app.FindCollectionByNameOrId("_pb_users_auth_")
if err != nil {
return err
}
// add field
if err := collection.Fields.AddMarshaledJSONAt(7, []byte(`{
"autogeneratePattern": "",
"hidden": false,
"id": "dlzhxcn2",
"max": 0,
"min": 0,
"name": "token",
"pattern": "",
"presentable": false,
"primaryKey": false,
"required": false,
"system": false,
"type": "text"
}`)); err != nil {
return err
}
return app.Save(collection)
})
}

View File

@@ -0,0 +1,138 @@
package migrations
import (
"encoding/json"
"github.com/pocketbase/pocketbase/core"
m "github.com/pocketbase/pocketbase/migrations"
)
func init() {
m.Register(func(app core.App) error {
jsonData := `{
"createRule": "user = @request.auth.id",
"deleteRule": "user = @request.auth.id",
"fields": [
{
"autogeneratePattern": "[a-z0-9]{15}",
"hidden": false,
"id": "text3208210256",
"max": 15,
"min": 15,
"name": "id",
"pattern": "^[a-z0-9]+$",
"presentable": false,
"primaryKey": true,
"required": true,
"system": true,
"type": "text"
},
{
"autogeneratePattern": "",
"hidden": false,
"id": "text1579384326",
"max": 0,
"min": 0,
"name": "name",
"pattern": "",
"presentable": false,
"primaryKey": false,
"required": true,
"system": false,
"type": "text"
},
{
"autogeneratePattern": "",
"hidden": true,
"id": "text1597481275",
"max": 64,
"min": 64,
"name": "token",
"pattern": "",
"presentable": false,
"primaryKey": false,
"required": true,
"system": false,
"type": "text"
},
{
"hidden": false,
"id": "date617435213",
"max": "",
"min": "",
"name": "expiration",
"presentable": false,
"required": false,
"system": false,
"type": "date"
},
{
"hidden": false,
"id": "date4016875332",
"max": "",
"min": "",
"name": "last_used",
"presentable": false,
"required": false,
"system": false,
"type": "date"
},
{
"cascadeDelete": false,
"collectionId": "_pb_users_auth_",
"hidden": false,
"id": "relation2375276105",
"maxSelect": 1,
"minSelect": 0,
"name": "user",
"presentable": false,
"required": true,
"system": false,
"type": "relation"
},
{
"hidden": false,
"id": "autodate2990389176",
"name": "created",
"onCreate": true,
"onUpdate": false,
"presentable": false,
"system": false,
"type": "autodate"
},
{
"hidden": false,
"id": "autodate3332085495",
"name": "updated",
"onCreate": true,
"onUpdate": true,
"presentable": false,
"system": false,
"type": "autodate"
}
],
"id": "pbc_3525142174",
"indexes": [],
"listRule": "user = @request.auth.id",
"name": "api_tokens",
"system": false,
"type": "base",
"updateRule": null,
"viewRule": "user = @request.auth.id"
}`
collection := &core.Collection{}
if err := json.Unmarshal([]byte(jsonData), &collection); err != nil {
return err
}
return app.Save(collection)
}, func(app core.App) error {
collection, err := app.FindCollectionByNameOrId("pbc_3525142174")
if err != nil {
return err
}
return app.Delete(collection)
})
}

View File

@@ -0,0 +1,178 @@
package migrations
import (
"encoding/json"
"github.com/pocketbase/dbx"
"github.com/pocketbase/pocketbase/core"
m "github.com/pocketbase/pocketbase/migrations"
)
func init() {
m.Register(func(app core.App) error {
jsonData := `{
"createRule": null,
"deleteRule": null,
"fields": [
{
"autogeneratePattern": "[a-z0-9]{15}",
"hidden": false,
"id": "text3208210256",
"max": 15,
"min": 15,
"name": "id",
"pattern": "^[a-z0-9]+$",
"presentable": false,
"primaryKey": true,
"required": true,
"system": true,
"type": "text"
},
{
"cascadeDelete": true,
"collectionId": "e864strfxo14pm4",
"hidden": false,
"id": "relation420001001",
"maxSelect": 1,
"minSelect": 0,
"name": "trail",
"presentable": false,
"required": true,
"system": false,
"type": "relation"
},
{
"hidden": false,
"id": "select420001002",
"maxSelect": 1,
"name": "provider",
"presentable": false,
"required": true,
"system": false,
"type": "select",
"values": [
"strava",
"komoot",
"hammerhead"
]
},
{
"autogeneratePattern": "",
"hidden": false,
"id": "text420001003",
"max": 255,
"min": 1,
"name": "external_id",
"pattern": "",
"presentable": false,
"primaryKey": false,
"required": true,
"system": false,
"type": "text"
},
{
"hidden": false,
"id": "autodate420001004",
"name": "created",
"onCreate": true,
"onUpdate": false,
"presentable": false,
"system": false,
"type": "autodate"
},
{
"hidden": false,
"id": "autodate420001005",
"name": "updated",
"onCreate": true,
"onUpdate": true,
"presentable": false,
"system": false,
"type": "autodate"
}
],
"id": "pbc_420001000",
"indexes": [
"CREATE UNIQUE INDEX ` + "`" + `idx_trail_external_reference_provider_external_id` + "`" + ` ON ` + "`" + `trail_external_reference` + "`" + ` (` + "`" + `provider` + "`" + `, ` + "`" + `external_id` + "`" + `)",
"CREATE UNIQUE INDEX ` + "`" + `idx_trail_external_reference_trail_provider_external_id` + "`" + ` ON ` + "`" + `trail_external_reference` + "`" + ` (` + "`" + `trail` + "`" + `, ` + "`" + `provider` + "`" + `, ` + "`" + `external_id` + "`" + `)"
],
"listRule": null,
"name": "trail_external_reference",
"system": false,
"type": "base",
"updateRule": null,
"viewRule": null
}`
collection := &core.Collection{}
if err := json.Unmarshal([]byte(jsonData), &collection); err != nil {
return err
}
if err := app.Save(collection); err != nil {
return err
}
referenceCollection, err := app.FindCollectionByNameOrId("trail_external_reference")
if err != nil {
return err
}
trails, err := app.FindRecordsByFilter(
"trails",
"external_provider != '' && external_id != ''",
"",
-1,
0,
nil,
)
if err != nil {
return err
}
for _, trail := range trails {
provider := trail.GetString("external_provider")
externalID := trail.GetString("external_id")
if provider == "" || externalID == "" {
continue
}
existing, err := app.FindRecordsByFilter(
"trail_external_reference",
"provider={:provider} && external_id={:external_id}",
"",
1,
0,
dbx.Params{
"provider": provider,
"external_id": externalID,
},
)
if err != nil {
return err
}
if len(existing) > 0 {
app.Logger().Warn("Skipping duplicate trail external reference during migration", "provider", provider, "external_id", externalID, "trail", trail.Id)
continue
}
record := core.NewRecord(referenceCollection)
record.Load(map[string]any{
"trail": trail.Id,
"provider": provider,
"external_id": externalID,
})
if err := app.Save(record); err != nil {
return err
}
}
return nil
}, func(app core.App) error {
collection, err := app.FindCollectionByNameOrId("pbc_420001000")
if err != nil {
return err
}
return app.Delete(collection)
})
}

View File

@@ -0,0 +1,62 @@
package migrations
import (
"github.com/pocketbase/pocketbase/core"
m "github.com/pocketbase/pocketbase/migrations"
)
func init() {
m.Register(func(app core.App) error {
collection, err := app.FindCollectionByNameOrId("e864strfxo14pm4")
if err != nil {
return err
}
collection.Fields.RemoveById("sajmiuau")
collection.Fields.RemoveById("htr35nha")
return app.Save(collection)
}, func(app core.App) error {
collection, err := app.FindCollectionByNameOrId("e864strfxo14pm4")
if err != nil {
return err
}
if err := collection.Fields.AddMarshaledJSONAt(17, []byte(`{
"autogeneratePattern": "",
"hidden": false,
"id": "sajmiuau",
"max": 0,
"min": 0,
"name": "external_id",
"pattern": "",
"presentable": false,
"primaryKey": false,
"required": false,
"system": false,
"type": "text"
}`)); err != nil {
return err
}
if err := collection.Fields.AddMarshaledJSONAt(18, []byte(`{
"hidden": false,
"id": "htr35nha",
"maxSelect": 1,
"name": "external_provider",
"presentable": false,
"required": false,
"system": false,
"type": "select",
"values": [
"strava",
"komoot",
"hammerhead"
]
}`)); err != nil {
return err
}
return app.Save(collection)
})
}

View File

@@ -0,0 +1,60 @@
package migrations
import (
"encoding/json"
"github.com/pocketbase/pocketbase/core"
m "github.com/pocketbase/pocketbase/migrations"
)
func init() {
m.Register(func(app core.App) error {
collection, err := app.FindCollectionByNameOrId("goeo2ubp103rzp9")
if err != nil {
return err
}
// add field
if err := collection.Fields.AddMarshaledJSONAt(8, []byte(`{
"exceptDomains": null,
"hidden": false,
"id": "url2434853685",
"name": "iri",
"onlyDomains": null,
"presentable": false,
"required": false,
"system": false,
"type": "url"
}`)); err != nil {
return err
}
// update collection data
if err := json.Unmarshal([]byte(`{
"indexes": [
"CREATE UNIQUE INDEX `+"`"+`idx_GgX6MsdCJq`+"`"+` ON `+"`"+`waypoints`+"`"+` (`+"`"+`iri`+"`"+`) WHERE iri IS NOT NULL AND iri != \"\";"
]
}`), &collection); err != nil {
return err
}
return app.Save(collection)
}, func(app core.App) error {
collection, err := app.FindCollectionByNameOrId("goeo2ubp103rzp9")
if err != nil {
return err
}
// update collection data
if err := json.Unmarshal([]byte(`{
"indexes": []
}`), &collection); err != nil {
return err
}
// remove field
collection.Fields.RemoveById("url2434853685")
return app.Save(collection)
})
}

View File

@@ -0,0 +1,78 @@
package migrations
import (
"github.com/pocketbase/pocketbase/core"
m "github.com/pocketbase/pocketbase/migrations"
)
func init() {
m.Register(func(app core.App) error {
collection, err := app.FindCollectionByNameOrId("goeo2ubp103rzp9")
if err != nil {
return err
}
// add field
if err := collection.Fields.AddMarshaledJSONAt(11, []byte(`{
"cascadeDelete": true,
"collectionId": "pbc_1295301207",
"hidden": false,
"id": "relation3182418120",
"maxSelect": 1,
"minSelect": 0,
"name": "author",
"presentable": false,
"required": true,
"system": false,
"type": "relation"
}`)); err != nil {
return err
}
// update field
if err := collection.Fields.AddMarshaledJSONAt(9, []byte(`{
"cascadeDelete": true,
"collectionId": "_pb_users_auth_",
"hidden": false,
"id": "8qbxrsd8",
"maxSelect": 1,
"minSelect": 0,
"name": "user",
"presentable": false,
"required": true,
"system": false,
"type": "relation"
}`)); err != nil {
return err
}
return app.Save(collection)
}, func(app core.App) error {
collection, err := app.FindCollectionByNameOrId("goeo2ubp103rzp9")
if err != nil {
return err
}
// remove field
collection.Fields.RemoveById("relation3182418120")
// update field
if err := collection.Fields.AddMarshaledJSONAt(9, []byte(`{
"cascadeDelete": true,
"collectionId": "_pb_users_auth_",
"hidden": false,
"id": "8qbxrsd8",
"maxSelect": 1,
"minSelect": 0,
"name": "author",
"presentable": false,
"required": true,
"system": false,
"type": "relation"
}`)); err != nil {
return err
}
return app.Save(collection)
})
}

View File

@@ -0,0 +1,33 @@
package migrations
import (
"github.com/pocketbase/pocketbase/core"
m "github.com/pocketbase/pocketbase/migrations"
)
func init() {
m.Register(func(app core.App) error {
wps, err := app.FindAllRecords("waypoints")
if err != nil {
return err
}
for _, wp := range wps {
actor, err := app.FindFirstRecordByData("activitypub_actors", "user", wp.GetString("user"))
if err != nil {
return err
}
wp.Set("author", actor.Id)
err = app.UnsafeWithoutHooks().Save(wp)
if err != nil {
return err
}
}
return nil
}, func(app core.App) error {
// add down queries...
return nil
})
}

View File

@@ -0,0 +1,44 @@
package migrations
import (
"github.com/pocketbase/pocketbase/core"
m "github.com/pocketbase/pocketbase/migrations"
)
func init() {
m.Register(func(app core.App) error {
collection, err := app.FindCollectionByNameOrId("goeo2ubp103rzp9")
if err != nil {
return err
}
// remove field
collection.Fields.RemoveById("8qbxrsd8")
return app.Save(collection)
}, func(app core.App) error {
collection, err := app.FindCollectionByNameOrId("goeo2ubp103rzp9")
if err != nil {
return err
}
// add field
if err := collection.Fields.AddMarshaledJSONAt(9, []byte(`{
"cascadeDelete": true,
"collectionId": "_pb_users_auth_",
"hidden": false,
"id": "8qbxrsd8",
"maxSelect": 1,
"minSelect": 0,
"name": "user",
"presentable": false,
"required": true,
"system": false,
"type": "relation"
}`)); err != nil {
return err
}
return app.Save(collection)
})
}

View File

@@ -0,0 +1,40 @@
package migrations
import (
"github.com/pocketbase/pocketbase/core"
m "github.com/pocketbase/pocketbase/migrations"
)
func init() {
m.Register(func(app core.App) error {
collection, err := app.FindCollectionByNameOrId("e864strfxo14pm4")
if err != nil {
return err
}
// add field
if err := collection.Fields.AddMarshaledJSONAt(23, []byte(`{
"hidden": false,
"id": "bool678597678",
"name": "needs_full_sync",
"presentable": false,
"required": false,
"system": false,
"type": "bool"
}`)); err != nil {
return err
}
return app.Save(collection)
}, func(app core.App) error {
collection, err := app.FindCollectionByNameOrId("e864strfxo14pm4")
if err != nil {
return err
}
// remove field
collection.Fields.RemoveById("bool678597678")
return app.Save(collection)
})
}

View File

@@ -0,0 +1,42 @@
package migrations
import (
"encoding/json"
"github.com/pocketbase/pocketbase/core"
m "github.com/pocketbase/pocketbase/migrations"
)
func init() {
m.Register(func(app core.App) error {
collection, err := app.FindCollectionByNameOrId("e864strfxo14pm4")
if err != nil {
return err
}
// update collection data
if err := json.Unmarshal([]byte(`{
"createRule": "@request.auth.id != \"\" && (@request.body.author.user = @request.auth.id)",
"updateRule": "author.user = @request.auth.id || (@request.auth.id != \"\" && trail_share_via_trail.trail = id && trail_share_via_trail.actor.user ?= @request.auth.id && trail_share_via_trail.permission = \"edit\")"
}`), &collection); err != nil {
return err
}
return app.Save(collection)
}, func(app core.App) error {
collection, err := app.FindCollectionByNameOrId("e864strfxo14pm4")
if err != nil {
return err
}
// update collection data
if err := json.Unmarshal([]byte(`{
"createRule": "@request.auth.id != \"\" && (@request.body.author.user = @request.auth.id || author.isLocal = false)",
"updateRule": "author.user = @request.auth.id || (@request.auth.id != \"\" && trail_share_via_trail.trail = id && trail_share_via_trail.actor.user ?= @request.auth.id && trail_share_via_trail.permission = \"edit\") || (@request.auth.id != \"\" && author.isLocal = false)"
}`), &collection); err != nil {
return err
}
return app.Save(collection)
})
}

View File

@@ -0,0 +1,42 @@
package migrations
import (
"encoding/json"
"github.com/pocketbase/pocketbase/core"
m "github.com/pocketbase/pocketbase/migrations"
)
func init() {
m.Register(func(app core.App) error {
collection, err := app.FindCollectionByNameOrId("r6gu2ajyidy1x69")
if err != nil {
return err
}
// update collection data
if err := json.Unmarshal([]byte(`{
"createRule": "@request.auth.id != \"\" && (@request.body.author.user = @request.auth.id)",
"updateRule": "author.user = @request.auth.id || (@request.auth.id != \"\" && list_share_via_list.list = id && list_share_via_list.actor.user ?= @request.auth.id && list_share_via_list.permission = \"edit\")"
}`), &collection); err != nil {
return err
}
return app.Save(collection)
}, func(app core.App) error {
collection, err := app.FindCollectionByNameOrId("r6gu2ajyidy1x69")
if err != nil {
return err
}
// update collection data
if err := json.Unmarshal([]byte(`{
"createRule": "@request.auth.id != \"\" && (@request.body.author.user = @request.auth.id || author.isLocal = false)",
"updateRule": "author.user = @request.auth.id || (@request.auth.id != \"\" && list_share_via_list.list = id && list_share_via_list.actor.user ?= @request.auth.id && list_share_via_list.permission = \"edit\") || (@request.auth.id != \"\" && author.isLocal = false)"
}`), &collection); err != nil {
return err
}
return app.Save(collection)
})
}

View File

@@ -0,0 +1,62 @@
package migrations
import (
"github.com/pocketbase/pocketbase/core"
m "github.com/pocketbase/pocketbase/migrations"
)
func init() {
m.Register(func(app core.App) error {
collection, err := app.FindCollectionByNameOrId("e864strfxo14pm4")
if err != nil {
return err
}
// add field
if err := collection.Fields.AddMarshaledJSONAt(5, []byte(`{
"hidden": false,
"id": "bool989355118",
"name": "completed",
"presentable": false,
"required": false,
"system": false,
"type": "bool"
}`)); err != nil {
return err
}
err = app.Save(collection)
if err != nil {
return err
}
logs, err := app.FindAllRecords("summit_logs")
if err != nil {
return err
}
for _, l := range logs {
trail, err := app.FindRecordById("trails", l.GetString("trail"))
if err != nil {
continue
}
trail.Set("completed", true)
err = app.Save(trail)
if err != nil {
return err
}
}
return nil
}, func(app core.App) error {
collection, err := app.FindCollectionByNameOrId("e864strfxo14pm4")
if err != nil {
return err
}
// remove field
collection.Fields.RemoveById("bool989355118")
return app.Save(collection)
})
}

View File

@@ -0,0 +1,88 @@
package migrations
import (
"github.com/pocketbase/pocketbase/core"
m "github.com/pocketbase/pocketbase/migrations"
)
func init() {
m.Register(func(app core.App) error {
collection, err := app.FindCollectionByNameOrId("pbc_1295301207")
if err != nil {
return err
}
// update field
if err := collection.Fields.AddMarshaledJSONAt(5, []byte(`{
"hidden": false,
"id": "number1386272118",
"max": null,
"min": null,
"name": "follower_count",
"onlyInt": true,
"presentable": false,
"required": false,
"system": false,
"type": "number"
}`)); err != nil {
return err
}
// update field
if err := collection.Fields.AddMarshaledJSONAt(6, []byte(`{
"hidden": false,
"id": "number3430500629",
"max": null,
"min": null,
"name": "following_count",
"onlyInt": true,
"presentable": false,
"required": false,
"system": false,
"type": "number"
}`)); err != nil {
return err
}
return app.Save(collection)
}, func(app core.App) error {
collection, err := app.FindCollectionByNameOrId("pbc_1295301207")
if err != nil {
return err
}
// update field
if err := collection.Fields.AddMarshaledJSONAt(5, []byte(`{
"hidden": false,
"id": "number1386272118",
"max": null,
"min": null,
"name": "followerCount",
"onlyInt": true,
"presentable": false,
"required": false,
"system": false,
"type": "number"
}`)); err != nil {
return err
}
// update field
if err := collection.Fields.AddMarshaledJSONAt(6, []byte(`{
"hidden": false,
"id": "number3430500629",
"max": null,
"min": null,
"name": "followingCount",
"onlyInt": true,
"presentable": false,
"required": false,
"system": false,
"type": "number"
}`)); err != nil {
return err
}
return app.Save(collection)
})
}

View File

@@ -0,0 +1,40 @@
package migrations
import (
"github.com/pocketbase/pocketbase/core"
m "github.com/pocketbase/pocketbase/migrations"
)
func init() {
m.Register(func(app core.App) error {
collection, err := app.FindCollectionByNameOrId("r6gu2ajyidy1x69")
if err != nil {
return err
}
// add field
if err := collection.Fields.AddMarshaledJSONAt(8, []byte(`{
"hidden": false,
"id": "bool678597678",
"name": "needs_full_sync",
"presentable": false,
"required": false,
"system": false,
"type": "bool"
}`)); err != nil {
return err
}
return app.Save(collection)
}, func(app core.App) error {
collection, err := app.FindCollectionByNameOrId("r6gu2ajyidy1x69")
if err != nil {
return err
}
// remove field
collection.Fields.RemoveById("bool678597678")
return app.Save(collection)
})
}

View File

@@ -0,0 +1,42 @@
package migrations
import (
"encoding/json"
"github.com/pocketbase/pocketbase/core"
m "github.com/pocketbase/pocketbase/migrations"
)
func init() {
m.Register(func(app core.App) error {
collection, err := app.FindCollectionByNameOrId("lf06qip3f4d11yk")
if err != nil {
return err
}
// update collection data
if err := json.Unmarshal([]byte(`{
"listRule": "((@request.auth.id != \"\" && trail.author.user = @request.auth.id) || trail.public = true) || author.user = @request.auth.id || trail.trail_share_via_trail.actor.user ?= @request.auth.id",
"viewRule": "((@request.auth.id != \"\" && trail.author.user = @request.auth.id) || trail.public = true) || author.user = @request.auth.id || trail.trail_share_via_trail.actor.user ?= @request.auth.id"
}`), &collection); err != nil {
return err
}
return app.Save(collection)
}, func(app core.App) error {
collection, err := app.FindCollectionByNameOrId("lf06qip3f4d11yk")
if err != nil {
return err
}
// update collection data
if err := json.Unmarshal([]byte(`{
"listRule": "((@request.auth.id != \"\" && trail.author.user = @request.auth.id) || trail.public = true) || author = @request.auth.id || trail.trail_share_via_trail.actor.user ?= @request.auth.id",
"viewRule": "((@request.auth.id != \"\" && trail.author.user = @request.auth.id) || trail.public = true) || author = @request.auth.id || trail.trail_share_via_trail.actor.user ?= @request.auth.id"
}`), &collection); err != nil {
return err
}
return app.Save(collection)
})
}

View File

@@ -0,0 +1,49 @@
package migrations
import (
"encoding/json"
"github.com/pocketbase/pocketbase/core"
m "github.com/pocketbase/pocketbase/migrations"
)
func init() {
m.Register(func(app core.App) error {
collection, err := app.FindCollectionByNameOrId("pbc_1295301207")
if err != nil {
return err
}
// update collection data
if err := json.Unmarshal([]byte(`{
"indexes": [
"CREATE UNIQUE INDEX `+"`"+`idx_rpT7QJwWTm`+"`"+` ON `+"`"+`activitypub_actors`+"`"+` (`+"`"+`iri`+"`"+`)",
"CREATE UNIQUE INDEX `+"`"+`idx_actors_username_domain`+"`"+` ON `+"`"+`activitypub_actors`+"`"+` (\n `+"`"+`preferred_username`+"`"+`,\n `+"`"+`domain`+"`"+`\n)",
"CREATE INDEX idx_activitypub_actors_user ON activitypub_actors(user) WHERE user IS NOT null;",
"CREATE INDEX `+"`"+`idx_x8xyfe8q8y`+"`"+` ON `+"`"+`activitypub_actors`+"`"+` (`+"`"+`inbox`+"`"+`)"
]
}`), &collection); err != nil {
return err
}
return app.Save(collection)
}, func(app core.App) error {
collection, err := app.FindCollectionByNameOrId("pbc_1295301207")
if err != nil {
return err
}
// update collection data
if err := json.Unmarshal([]byte(`{
"indexes": [
"CREATE UNIQUE INDEX `+"`"+`idx_rpT7QJwWTm`+"`"+` ON `+"`"+`activitypub_actors`+"`"+` (`+"`"+`iri`+"`"+`)",
"CREATE INDEX idx_actors_username_domain\nON activitypub_actors(preferred_username, domain);",
"CREATE INDEX idx_activitypub_actors_user ON activitypub_actors(user);"
]
}`), &collection); err != nil {
return err
}
return app.Save(collection)
})
}

View File

@@ -0,0 +1,82 @@
package migrations
import (
"encoding/json"
"github.com/pocketbase/pocketbase/core"
m "github.com/pocketbase/pocketbase/migrations"
)
func init() {
m.Register(func(app core.App) error {
collection, err := app.FindCollectionByNameOrId("4wbv9tz5zjdrjh1")
if err != nil {
return err
}
// update collection data
if err := json.Unmarshal([]byte(`{
"viewQuery": "SELECT\n a.id,\n a.user,\n COALESCE(printf(\"%.2f\", MAX(t.max_distance)), 0) AS max_distance,\n COALESCE(printf(\"%.2f\", MAX(t.max_elevation_gain)), 0) AS max_elevation_gain,\n COALESCE(printf(\"%.2f\", MAX(t.max_elevation_loss)), 0) AS max_elevation_loss,\n COALESCE(printf(\"%.2f\", MAX(t.max_duration)), 0) AS max_duration,\n COALESCE(printf(\"%.2f\", MIN(t.min_distance)), 0) AS min_distance,\n COALESCE(printf(\"%.2f\", MIN(t.min_elevation_gain)), 0) AS min_elevation_gain,\n COALESCE(printf(\"%.2f\", MIN(t.min_elevation_loss)), 0) AS min_elevation_loss,\n COALESCE(printf(\"%.2f\", MIN(t.min_duration)), 0) AS min_duration\nFROM activitypub_actors a\nLEFT JOIN (\n SELECT author AS actor_id,\n MAX(distance) AS max_distance,\n MIN(distance) AS min_distance,\n MAX(elevation_gain) AS max_elevation_gain,\n MIN(elevation_gain) AS min_elevation_gain,\n MAX(elevation_loss) AS max_elevation_loss,\n MIN(elevation_loss) AS min_elevation_loss,\n MAX(duration) AS max_duration,\n MIN(duration) AS min_duration\n FROM trails\n GROUP BY author\n UNION ALL\n SELECT ts.actor AS actor_id,\n MAX(t.distance) AS max_distance,\n MIN(t.distance) AS min_distance,\n MAX(t.elevation_gain) AS max_elevation_gain,\n MIN(t.elevation_gain) AS min_elevation_gain,\n MAX(t.elevation_loss) AS max_elevation_loss,\n MIN(t.elevation_loss) AS min_elevation_loss,\n MAX(t.duration) AS max_duration,\n MIN(t.duration) AS min_duration\n FROM trail_share ts\n JOIN trails t ON t.id = ts.trail\n GROUP BY ts.actor\n UNION ALL\n SELECT a2.id AS actor_id,\n p.max_distance,\n p.min_distance,\n p.max_elevation_gain,\n p.min_elevation_gain,\n p.max_elevation_loss,\n p.min_elevation_loss,\n p.max_duration,\n p.min_duration\n FROM activitypub_actors a2\n CROSS JOIN (\n SELECT\n MAX(distance) AS max_distance,\n MIN(distance) AS min_distance,\n MAX(elevation_gain) AS max_elevation_gain,\n MIN(elevation_gain) AS min_elevation_gain,\n MAX(elevation_loss) AS max_elevation_loss,\n MIN(elevation_loss) AS min_elevation_loss,\n MAX(duration) AS max_duration,\n MIN(duration) AS min_duration\n FROM trails\n WHERE public = TRUE\n ) p\n) t ON t.actor_id = a.id\nWHERE a.user != \"\"\nGROUP BY a.id;"
}`), &collection); err != nil {
return err
}
// remove field
collection.Fields.RemoveById("_clone_rQGp")
// add field
if err := collection.Fields.AddMarshaledJSONAt(1, []byte(`{
"cascadeDelete": true,
"collectionId": "_pb_users_auth_",
"help": "",
"hidden": false,
"id": "_clone_bYSp",
"maxSelect": 1,
"minSelect": 0,
"name": "user",
"presentable": false,
"required": false,
"system": false,
"type": "relation"
}`)); err != nil {
return err
}
return app.Save(collection)
}, func(app core.App) error {
collection, err := app.FindCollectionByNameOrId("4wbv9tz5zjdrjh1")
if err != nil {
return err
}
// update collection data
if err := json.Unmarshal([]byte(`{
"viewQuery": "SELECT\n a.id,\n a.user,\n COALESCE(printf(\"%.2f\", MAX(t.max_distance)), 0) AS max_distance,\n COALESCE(printf(\"%.2f\", MAX(t.max_elevation_gain)), 0) AS max_elevation_gain,\n COALESCE(printf(\"%.2f\", MAX(t.max_elevation_loss)), 0) AS max_elevation_loss,\n COALESCE(printf(\"%.2f\", MAX(t.max_duration)), 0) AS max_duration,\n COALESCE(printf(\"%.2f\", MIN(t.min_distance)), 0) AS min_distance,\n COALESCE(printf(\"%.2f\", MIN(t.min_elevation_gain)), 0) AS min_elevation_gain,\n COALESCE(printf(\"%.2f\", MIN(t.min_elevation_loss)), 0) AS min_elevation_loss,\n COALESCE(printf(\"%.2f\", MIN(t.min_duration)), 0) AS min_duration\nFROM activitypub_actors a\nLEFT JOIN (\n SELECT author AS actor_id,\n MAX(distance) AS max_distance,\n MIN(distance) AS min_distance,\n MAX(elevation_gain) AS max_elevation_gain,\n MIN(elevation_gain) AS min_elevation_gain,\n MAX(elevation_loss) AS max_elevation_loss,\n MIN(elevation_loss) AS min_elevation_loss,\n MAX(duration) AS max_duration,\n MIN(duration) AS min_duration\n FROM trails\n GROUP BY author\n UNION ALL\n SELECT ts.actor AS actor_id,\n MAX(t.distance) AS max_distance,\n MIN(t.distance) AS min_distance,\n MAX(t.elevation_gain) AS max_elevation_gain,\n MIN(t.elevation_gain) AS min_elevation_gain,\n MAX(t.elevation_loss) AS max_elevation_loss,\n MIN(t.elevation_loss) AS min_elevation_loss,\n MAX(t.duration) AS max_duration,\n MIN(t.duration) AS min_duration\n FROM trail_share ts\n JOIN trails t ON t.id = ts.trail\n GROUP BY ts.actor\n UNION ALL\n SELECT a2.id AS actor_id,\n p.max_distance,\n p.min_distance,\n p.max_elevation_gain,\n p.min_elevation_gain,\n p.max_elevation_loss,\n p.min_elevation_loss,\n p.max_duration,\n p.min_duration\n FROM activitypub_actors a2\n CROSS JOIN (\n SELECT\n MAX(distance) AS max_distance,\n MIN(distance) AS min_distance,\n MAX(elevation_gain) AS max_elevation_gain,\n MIN(elevation_gain) AS min_elevation_gain,\n MAX(elevation_loss) AS max_elevation_loss,\n MIN(elevation_loss) AS min_elevation_loss,\n MAX(duration) AS max_duration,\n MIN(duration) AS min_duration\n FROM trails\n WHERE public = TRUE\n ) p\n) t ON t.actor_id = a.id\nWHERE a.user is not null\nGROUP BY a.id;"
}`), &collection); err != nil {
return err
}
// add field
if err := collection.Fields.AddMarshaledJSONAt(1, []byte(`{
"cascadeDelete": true,
"collectionId": "_pb_users_auth_",
"help": "",
"hidden": false,
"id": "_clone_rQGp",
"maxSelect": 1,
"minSelect": 0,
"name": "user",
"presentable": false,
"required": false,
"system": false,
"type": "relation"
}`)); err != nil {
return err
}
// remove field
collection.Fields.RemoveById("_clone_bYSp")
return app.Save(collection)
})
}

View File

@@ -0,0 +1,82 @@
package migrations
import (
"encoding/json"
"github.com/pocketbase/pocketbase/core"
m "github.com/pocketbase/pocketbase/migrations"
)
func init() {
m.Register(func(app core.App) error {
collection, err := app.FindCollectionByNameOrId("urytyc428mwlbqq")
if err != nil {
return err
}
// update collection data
if err := json.Unmarshal([]byte(`{
"viewQuery": "SELECT \n a.id, a.user, \n COALESCE(MAX(t.max_lat), 0) AS max_lat, \n COALESCE(MAX(t.max_lon), 0) AS max_lon, \n COALESCE(MIN(t.min_lat), 0) AS min_lat, \n COALESCE(MIN(t.min_lon), 0) AS min_lon \nFROM activitypub_actors a \nLEFT JOIN ( \n SELECT author AS actor_id, \n MAX(lat) AS max_lat, \n MAX(lon) AS max_lon, \n MIN(lat) AS min_lat, \n MIN(lon) AS min_lon \n FROM trails \n GROUP BY author \n UNION ALL \n SELECT ts.actor AS actor_id, \n MAX(t.lat) AS max_lat, \n MAX(t.lon) AS max_lon, \n MIN(t.lat) AS min_lat, \n MIN(t.lon) AS min_lon \n FROM trail_share ts \n JOIN trails t ON t.id = ts.trail \n GROUP BY ts.actor \n UNION ALL \n SELECT a2.id AS actor_id, \n p.max_lat, p.max_lon, p.min_lat, p.min_lon \n FROM activitypub_actors a2 \n CROSS JOIN ( \n SELECT \n MAX(lat) AS max_lat, \n MAX(lon) AS max_lon, \n MIN(lat) AS min_lat, \n MIN(lon) AS min_lon \n FROM trails \n WHERE public = TRUE \n ) p \n) t ON t.actor_id = a.id \nWHERE a.user != \"\"\nGROUP BY a.id;"
}`), &collection); err != nil {
return err
}
// remove field
collection.Fields.RemoveById("_clone_MXPc")
// add field
if err := collection.Fields.AddMarshaledJSONAt(1, []byte(`{
"cascadeDelete": true,
"collectionId": "_pb_users_auth_",
"help": "",
"hidden": false,
"id": "_clone_b2Wa",
"maxSelect": 1,
"minSelect": 0,
"name": "user",
"presentable": false,
"required": false,
"system": false,
"type": "relation"
}`)); err != nil {
return err
}
return app.Save(collection)
}, func(app core.App) error {
collection, err := app.FindCollectionByNameOrId("urytyc428mwlbqq")
if err != nil {
return err
}
// update collection data
if err := json.Unmarshal([]byte(`{
"viewQuery": "SELECT \n a.id, a.user, \n COALESCE(MAX(t.max_lat), 0) AS max_lat, \n COALESCE(MAX(t.max_lon), 0) AS max_lon, \n COALESCE(MIN(t.min_lat), 0) AS min_lat, \n COALESCE(MIN(t.min_lon), 0) AS min_lon \nFROM activitypub_actors a \nLEFT JOIN ( \n SELECT author AS actor_id, \n MAX(lat) AS max_lat, \n MAX(lon) AS max_lon, \n MIN(lat) AS min_lat, \n MIN(lon) AS min_lon \n FROM trails \n GROUP BY author \n UNION ALL \n SELECT ts.actor AS actor_id, \n MAX(t.lat) AS max_lat, \n MAX(t.lon) AS max_lon, \n MIN(t.lat) AS min_lat, \n MIN(t.lon) AS min_lon \n FROM trail_share ts \n JOIN trails t ON t.id = ts.trail \n GROUP BY ts.actor \n UNION ALL \n SELECT a2.id AS actor_id, \n p.max_lat, p.max_lon, p.min_lat, p.min_lon \n FROM activitypub_actors a2 \n CROSS JOIN ( \n SELECT \n MAX(lat) AS max_lat, \n MAX(lon) AS max_lon, \n MIN(lat) AS min_lat, \n MIN(lon) AS min_lon \n FROM trails \n WHERE public = TRUE \n ) p \n) t ON t.actor_id = a.id \nGROUP BY a.id;"
}`), &collection); err != nil {
return err
}
// add field
if err := collection.Fields.AddMarshaledJSONAt(1, []byte(`{
"cascadeDelete": true,
"collectionId": "_pb_users_auth_",
"help": "",
"hidden": false,
"id": "_clone_MXPc",
"maxSelect": 1,
"minSelect": 0,
"name": "user",
"presentable": false,
"required": false,
"system": false,
"type": "relation"
}`)); err != nil {
return err
}
// remove field
collection.Fields.RemoveById("_clone_b2Wa")
return app.Save(collection)
})
}

Some files were not shown because too many files have changed in this diff Show More