3 Commits

Author SHA1 Message Date
Christian Beutel
920c8da612 fixes ux and adds i18n 2025-06-26 13:21:51 +02:00
Christian Beutel
932f9a3888 Merge remote-tracking branch 'origin/main' into brian/trail-card-tags 2025-06-26 13:03:28 +02:00
briannelson95
07c69271f4 Updated trail card to show first 2 tags and made tags smaller 2025-04-30 09:10:59 -04:00
544 changed files with 52395 additions and 64383 deletions

View File

@@ -1,24 +0,0 @@
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

View File

@@ -1,8 +0,0 @@
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.

View File

@@ -1,83 +0,0 @@
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

View File

@@ -1,33 +0,0 @@
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,97 +1,139 @@
name: Publish Release
name: Release Workflow
on:
pull_request:
types: [closed]
branches: [main]
workflow_dispatch:
inputs:
version:
description: "The version to release (e.g., 0.12.0)"
required: true
jobs:
# 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')
# Job 1: Bump Versions and Create Tags
versioning:
runs-on: ubuntu-latest
permissions:
contents: write
outputs:
version: ${{ steps.get_version.outputs.version }}
version: ${{ steps.set_version.outputs.version }}
steps:
# 1. Checkout the repository
- name: Checkout code
uses: actions/checkout@v6
- 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 tag $VERSION_TAG
git push origin $VERSION_TAG
docker-build:
needs: publish
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: Setup Go
uses: actions/setup-go@v6
with:
go-version: '1.22'
uses: actions/checkout@v3
# 2. Setup node & npm
- name: Setup Node.js
uses: actions/setup-node@v6
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
run: |
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
# Job 2: Build Docker Images
docker-build:
runs-on: ubuntu-latest
needs: versioning
steps:
# 1. Checkout the repository
- name: Checkout code
uses: actions/checkout@v3
with:
ref: ${{ github.ref }}
- name: Setup Go
uses: actions/setup-go@v4
with:
go-version: '1.22'
# 2. Log in to Docker Hub
- name: Login to Docker Hub
uses: docker/login-action@v4
uses: docker/login-action@v3
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@v4
uses: docker/setup-buildx-action@v3
# 4. Build and push Docker images
- name: Build Docker Images
env:
VERSION: ${{ needs.publish.outputs.version }}
VERSION: ${{ needs.versioning.outputs.version }}
run: |
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 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 web/ --no-cache -t flomp/wanderer-web:$VERSION -t flomp/wanderer-web: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
permissions:
contents: write
needs: [versioning, docker-build]
steps:
- uses: actions/checkout@v6
# 1. Checkout the repository
- name: Checkout code
uses: actions/checkout@v3
with:
ref: ${{ github.ref }}
# 2. Extract release notes from CHANGELOG.md
- name: Extract release notes
id: changelog
run: |
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)
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)
echo 'changelog<<EOF' >> $GITHUB_OUTPUT
printf "$CHANGELOG" >> $GITHUB_OUTPUT
printf "%s\n" "$CHANGELOG" >> $GITHUB_OUTPUT
echo 'EOF' >> $GITHUB_OUTPUT
# 3. Create GitHub Release
- name: Create GitHub Release
uses: softprops/action-gh-release@v2
uses: actions/create-release@v1
with:
tag_name: ${{ needs.publish.outputs.version }}
tag_name: ${{ needs.versioning.outputs.version }}
release_name: "${{ needs.versioning.outputs.version }}"
body: ${{ steps.changelog.outputs.changelog }}
draft: false
prerelease: false
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -1,46 +0,0 @@
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

View File

@@ -1,39 +0,0 @@
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

View File

@@ -1,38 +0,0 @@
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,227 +1,10 @@
# 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/open-wanderer/wanderer/blob/main/docker-compose.yml) for a valid configuration.
Check the reopsitory's [`docker-compose.yml`](https://github.com/Flomp/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.
@@ -335,7 +118,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
## Bug fixes
## Bugfixes
- 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
@@ -470,7 +253,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/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).
- 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).
## Features
- Adds password reset email function for users (see [docs](https://wanderer.to/guides/authentication/#forgot-your-password) for more info)
@@ -544,7 +327,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
- Fixes map height when viewing a trail in detail view
- Fixed map height when viewing a trail in detail view
# v0.8.1
## Features
@@ -621,7 +404,7 @@ As the number of contributors to this project continues to grow (which Im ver
- Adds missing translations
## Bug fixes
- Fixes a bug that prevented comments from showing up
- Fixed 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
@@ -629,7 +412,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
- Fixes a bug that would prevent users from creating multiple summit log entries without reloading the page
- Fixed 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
@@ -641,7 +424,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/open-wanderer/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/Flomp/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
@@ -655,13 +438,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
- 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
- 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
# v0.3.2
## Bug fixes
- Fixes a bug that caused a 500 Internal Error to appear when viewing trails without an account
- Fixed a bug that caused a 500 Internal Error to appear when viewing trails without an account
## Translations
@@ -676,7 +459,7 @@ As the number of contributors to this project continues to grow (which Im ver
## Bug fixes
- Fixes a bug that prevented import trails from appearing in map view
- Fixed a bug that prevented import trails from appearing in map view
## Translations
@@ -690,13 +473,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/open-wanderer/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/Flomp/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/open-wanderer/wanderer/wiki/OAuth) how to enable providers.
- added OAuth support. Read [here](https://github.com/Flomp/wanderer/wiki/OAuth) how to enable providers.
## Bug fixes
- Fixes a bug that would show a wrong date for summit logs for certain time zones
- fixed a bug that would show a wrong date for summit logs for certain time zones
- added client side validation for usernames
## Translations
@@ -708,7 +491,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
- Fixes a bug that would overwrite trail names and descriptions when editing a trail
- fixed a bug that would overwrite trail names and descriptions when editing a trail
## Translations
@@ -725,7 +508,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/open-wanderer/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/Flomp/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.
@@ -737,10 +520,10 @@ As the number of contributors to this project continues to grow (which Im ver
# v0.1.1
## Bug fixes
- Fixes a bug that would prevent trails longer than 20km from being displayed
- fixed 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
- 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
- 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
- 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
@@ -749,4 +532,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

View File

@@ -1,43 +0,0 @@
## 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/open-wanderer/wanderer/)
[![GitHub Repo stars](https://img.shields.io/github/stars/open-wanderer/wanderer?style=social)](https://github.com/open-wanderer/wanderer/)
[![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/)
[![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/open-wanderer/wanderer/main/docker-compose.yml
wget https://raw.githubusercontent.com/Flomp/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/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/#installation-from-source) for a detailed how-to guide.
## Support wanderer

View File

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

View File

@@ -1,56 +1,18 @@
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
FROM alpine:3.16
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
ENV MEILI_URL=http://localhost:7700 \
MEILI_MASTER_KEY= \
POCKETBASE_ENCRYPTION_KEY=
ARG TARGETARCH
RUN echo ${TARGETARCH}
COPY ./pocketbase_${TARGETARCH} /pocketbase
RUN chmod +x /pocketbase
ENV MEILI_URL=http://localhost:7700
ENV MEILI_MASTER_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"]

View File

@@ -1,121 +0,0 @@
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,9 +4,12 @@ import (
"bytes"
"context"
"crypto/x509"
"database/sql"
"encoding/pem"
"fmt"
"io"
"net/http"
"net/url"
"os"
"slices"
"strings"
@@ -24,103 +27,196 @@ import (
)
func PostActivity(app core.App, actor *core.Record, activity *pub.Activity, recipients []string) error {
go func() {
defer func() {
if r := recover(); r != nil {
app.Logger().Error(fmt.Sprintf("Recovered from panic in PostActivity: %v", r))
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
}
}()
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
}
if err := sem.Acquire(context.Background(), 1); err != nil {
app.Logger().Error(fmt.Sprintf("Semaphore acquire failed: %s", err))
return
}
defer sem.Release(1)
algs := []httpsig.Algorithm{httpsig.RSA_SHA256}
postHeaders := []string{"(request-target)", "Date", "Digest", "Content-Type", "Host"}
expiresIn := 60
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)
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
}
if err := signer.SignRequest(privateKey, pubID, req, body); err != nil {
app.Logger().Error(fmt.Sprintf("Signing request failed: %s", err))
return
}
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"
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()
client := &http.Client{}
sem := semaphore.NewWeighted(5)
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)
}
slices.Sort(recipients)
uniqueRecipients := slices.Compact(recipients)
}(v)
}
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()
}()
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,17 +1,13 @@
package federation
import (
"context"
"crypto/x509"
"database/sql"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"os"
"pocketbase/util"
"strings"
"time"
@@ -23,9 +19,6 @@ 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 {
@@ -34,36 +27,24 @@ type WebfingerResponse struct {
} `json:"links"`
}
func validateActorResponse(actor *pub.Actor) error {
if actor == nil {
return ErrInvalidActorResponse
func SplitHandle(handle string) (string, string) {
cleaned := strings.TrimPrefix(handle, "@")
cleaned = strings.TrimSpace(cleaned)
if !strings.Contains(cleaned, "@") {
return cleaned, ""
}
if actor.GetID().String() == "" {
return fmt.Errorf("%w: missing ID", ErrInvalidActorResponse)
}
parts := strings.SplitN(cleaned, "@", 2)
user := parts[0]
domain := parts[1]
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
return user, domain
}
func GetActorByHandle(app core.App, ctx context.Context, handle string, includeFollows bool) (*core.Record, error) {
username, domain := util.SplitHandle(handle)
func GetActorByHandle(app core.App, actor *core.Record, handle string, includeFollows bool) (*core.Record, error) {
username, domain := SplitHandle(handle)
filter := "preferred_username={:username}&&"
if domain != "" {
@@ -82,7 +63,7 @@ func GetActorByHandle(app core.App, ctx context.Context, handle string, includeF
dbActor = core.NewRecord(collection)
dbActor.Set("isLocal", false)
iri, err := iriFromHandle(ctx, domain, username)
iri, err := iriFromHandle(domain, username)
if err != nil {
return nil, err
}
@@ -92,10 +73,10 @@ func GetActorByHandle(app core.App, ctx context.Context, handle string, includeF
return nil, err
}
return assembleActor(app, ctx, dbActor, includeFollows || dbActor.Id == "")
return assembleActor(actor, dbActor, app, includeFollows)
}
func GetActorByIRI(app core.App, ctx context.Context, iri string, includeFollows bool) (*core.Record, error) {
func GetActorByIRI(app core.App, actor *core.Record, 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 {
@@ -112,55 +93,33 @@ func GetActorByIRI(app core.App, ctx context.Context, iri string, includeFollows
return nil, err
}
return assembleActor(app, ctx, dbActor, includeFollows || dbActor.Id == "")
return assembleActor(actor, dbActor, app, includeFollows)
}
func iriFromHandle(ctx context.Context, domain string, username string) (string, error) {
client := util.SafeHTTPClient()
func iriFromHandle(domain string, username string) (string, error) {
client := &http.Client{}
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)
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)
}
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(limitedReader).Decode(&wf); err != nil {
return "", fmt.Errorf("failed to decode JSON: %w", err)
if err := json.NewDecoder(resp.Body).Decode(&wf); err != nil {
return "", 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(app core.App, ctx context.Context, dbActor *core.Record, includeFollows bool) (*core.Record, error) {
func assembleActor(actor *core.Record, dbActor *core.Record, app core.App, includeFollows bool) (*core.Record, error) {
origin := os.Getenv("ORIGIN")
if origin == "" {
return nil, fmt.Errorf("ORIGIN environment variable not set")
@@ -181,16 +140,16 @@ func assembleActor(app core.App, ctx context.Context, dbActor *core.Record, incl
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} AND status='accepted'", dbx.Params{"user": dbActor.Id}))
followerCount, err := app.CountRecords("follows", dbx.NewExp("followee={:user}", dbx.Params{"user": dbActor.Id}))
if err != nil {
return nil, err
}
dbActor.Set("follower_count", followerCount)
followingCount, err := app.CountRecords("follows", dbx.NewExp("follower={:user} AND status='accepted'", dbx.Params{"user": dbActor.Id}))
dbActor.Set("followerCount", followerCount)
followingCount, err := app.CountRecords("follows", dbx.NewExp("follower={:user}", dbx.Params{"user": dbActor.Id}))
if err != nil {
return nil, err
}
dbActor.Set("following_count", followingCount)
dbActor.Set("followingCount", followingCount)
dbActor.Set("last_fetched", time.Now())
@@ -198,17 +157,16 @@ func assembleActor(app core.App, ctx context.Context, dbActor *core.Record, incl
result := make(map[string]interface{})
json.Unmarshal([]byte(privacy), &result)
// check that it's not our own profile
private = result["account"] == "private" && dbActor.Id != strings.TrimPrefix(ctx.Value("actor").(string), "actor:")
private = result["account"] == "private"
} else {
// check if value is still cached
twoHoursAgo := time.Now().UTC().Add(-2 * time.Hour)
if dbActor.GetDateTime("last_fetched").Time().After(twoHoursAgo) {
twoHoursAgo := time.Now().Add(-2 * time.Hour)
if !includeFollows && dbActor.GetDateTime("last_fetched").Time().After(twoHoursAgo) {
return dbActor, nil
}
pubActor, followers, following, err := fetchRemoteActor(app, ctx, dbActor.GetString("iri"), includeFollows)
pubActor, followers, following, err := fetchRemoteActor(actor, dbActor.GetString("iri"), includeFollows)
if err != nil {
if dbActor.Id != "" {
return dbActor, err
@@ -230,55 +188,53 @@ func assembleActor(app core.App, ctx context.Context, dbActor *core.Record, incl
}
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", util.ItemID(pubActor.Followers))
dbActor.Set("inbox", util.ItemID(pubActor.Inbox))
dbActor.Set("followers", pubActor.Followers.GetID().String())
dbActor.Set("inbox", pubActor.Inbox.GetID().String())
dbActor.Set("iri", pubActor.GetID().String())
dbActor.Set("username", pubActor.Name.String())
dbActor.Set("preferred_username", pubActor.PreferredUsername.String())
dbActor.Set("following", util.ItemID(pubActor.Following))
dbActor.Set("following", pubActor.Following.GetID().String())
dbActor.Set("summary", pubActor.Summary.String())
dbActor.Set("outbox", util.ItemID(pubActor.Outbox))
dbActor.Set("outbox", pubActor.Outbox.GetID().String())
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("follower_count", int(followers.TotalItems))
dbActor.Set("following_count", int(following.TotalItems))
dbActor.Set("followerCount", int(followers.TotalItems))
dbActor.Set("followingCount", int(following.TotalItems))
}
}
err := app.Save(dbActor)
if err != nil {
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 {
return nil, err
}
if private {
return dbActor, ErrProfilePrivate
return dbActor, fmt.Errorf("profile is private")
}
return dbActor, nil
}
// Fetches an AP actor and optionally followers/following collections
func fetchRemoteActor(app core.App, ctx context.Context, iri string, includeFollows bool) (*pub.Actor, *pub.OrderedCollection, *pub.OrderedCollection, error) {
func fetchRemoteActor(actor *core.Record, 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 := util.SafeHTTPClient()
req, err := http.NewRequestWithContext(ctx, "GET", iri, nil)
client := &http.Client{}
req, _ := http.NewRequest("GET", iri, nil)
headers := map[string]string{
"Accept": "application/ld+json",
@@ -291,11 +247,8 @@ func fetchRemoteActor(app core.App, ctx context.Context, iri string, includeFoll
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")
dbPrivateKey := actor.GetString("private_key")
if dbPrivateKey != "" {
algs := []httpsig.Algorithm{httpsig.RSA_SHA256}
postHeaders := []string{"(request-target)", "Date", "Digest", "Content-Type", "Host"}
expiresIn := 60
@@ -314,7 +267,7 @@ func fetchRemoteActor(app core.App, ctx context.Context, iri string, includeFoll
return nil, nil, nil, err
}
pubID := userActor.GetString("iri") + "#main-key"
pubID := actor.GetString("iri") + "#main-key"
if err := signer.SignRequest(privateKey, pubID, req, []byte{}); err != nil {
return nil, nil, nil, err
@@ -336,21 +289,16 @@ func fetchRemoteActor(app core.App, ctx context.Context, iri string, includeFoll
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(app, ctx, util.ItemID(pubActor.Followers)); err == nil {
if data, err := FetchCollection(actor, pubActor.Followers.GetID().String()); err == nil {
followers = *data
}
// Fetch following
if data, err := FetchCollection(app, ctx, util.ItemID(pubActor.Following)); err == nil {
if data, err := FetchCollection(actor, pubActor.Following.GetID().String()); err == nil {
following = *data
}
}
@@ -358,13 +306,13 @@ func fetchRemoteActor(app core.App, ctx context.Context, iri string, includeFoll
return &pubActor, &followers, &following, nil
}
func FetchCollection(app core.App, ctx context.Context, collectionURL string) (*pub.OrderedCollection, error) {
func FetchCollection(actor *core.Record, url string) (*pub.OrderedCollection, error) {
encryptionKey := os.Getenv("POCKETBASE_ENCRYPTION_KEY")
if len(encryptionKey) == 0 {
return nil, fmt.Errorf("POCKETBASE_ENCRYPTION_KEY not set")
}
req, err := http.NewRequestWithContext(ctx, "GET", collectionURL, nil)
req, _ := http.NewRequest("GET", url, nil)
headers := map[string]string{
"Accept": "application/ld+json",
@@ -376,48 +324,38 @@ func FetchCollection(app core.App, ctx context.Context, collectionURL string) (*
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
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
}
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 := actor.GetString("iri") + "#main-key"
if err := signer.SignRequest(privateKey, pubID, req, []byte{}); err != nil {
return nil, 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)
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)
}
defer resp.Body.Close()

View File

@@ -21,7 +21,7 @@ import (
"golang.org/x/net/html"
)
func CreateTrailActivity(app core.App, ctx context.Context, trail *core.Record, typ pub.ActivityVocabularyType) error {
func CreateTrailActivity(app core.App, actor *core.Record, 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, ctx context.Context, trail *core.Record,
id := fmt.Sprintf("%s/api/v1/activitypub/activity/%s", origin, recordId)
to := "https://www.w3.org/ns/activitystreams#Public"
mentionedActors, err := ActorsFromMentions(app, ctx, trail.GetString("description"))
mentionedActors, handles, err := ActorsFromMentions(app, actor, trail.GetString("description"))
if err != nil {
return err
}
@@ -54,11 +54,11 @@ func CreateTrailActivity(app core.App, ctx context.Context, trail *core.Record,
mentions := []string{}
cc := pub.ItemCollection{pub.IRI(trailAuthor.GetString("followers"))}
tags := pub.ItemCollection{}
for _, m := range mentionedActors {
for i, 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, fmt.Sprintf("@%s@%s", m.GetString("preferred_username"), m.GetString("domain"))))
mention.Name = pub.NaturalLanguageValuesNew(pub.LangRefValueNew(pub.NilLangRef, handles[i]))
tags.Append(mention)
mentions = append(mentions, inbox)
@@ -108,7 +108,7 @@ func CreateTrailActivity(app core.App, ctx context.Context, trail *core.Record,
return PostActivity(app, trailAuthor, activity, recipients)
}
func CreateCommentActivity(app core.App, ctx context.Context, comment *core.Record, typ pub.ActivityVocabularyType) error {
func CreateCommentActivity(app core.App, actor *core.Record, 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, ctx context.Context, comment *core.Reco
id := fmt.Sprintf("%s/api/v1/activitypub/activity/%s", origin, activityRecordId)
to := "https://www.w3.org/ns/activitystreams#Public"
mentionedActors, err := ActorsFromMentions(app, ctx, comment.GetString("text"))
mentionedActors, handles, err := ActorsFromMentions(app, actor, comment.GetString("text"))
if err != nil {
return err
}
recipients := []string{}
tags := pub.ItemCollection{}
for _, m := range mentionedActors {
for i, 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, fmt.Sprintf("@%s@%s", m.GetString("preferred_username"), m.GetString("domain"))))
mention.Name = pub.NaturalLanguageValuesNew(pub.LangRefValueNew(pub.NilLangRef, handles[i]))
tags.Append(mention)
recipients = append(recipients, m.GetString("inbox"))
@@ -193,7 +193,7 @@ func CreateCommentActivity(app core.App, ctx context.Context, comment *core.Reco
}
func CreateSummitLogActivity(app core.App, ctx context.Context, summitLog *core.Record, typ pub.ActivityVocabularyType) error {
func CreateSummitLogActivity(app core.App, actor *core.Record, summitLog *core.Record, typ pub.ActivityVocabularyType) error {
origin := os.Getenv("ORIGIN")
if origin == "" {
@@ -245,7 +245,7 @@ func CreateSummitLogActivity(app core.App, ctx context.Context, summitLog *core.
to.Append(pub.IRI(summitLogTrailAuthor.GetString("iri")))
}
mentionedActors, err := ActorsFromMentions(app, ctx, summitLog.GetString("text"))
mentionedActors, handles, err := ActorsFromMentions(app, actor, summitLog.GetString("text"))
if err != nil {
return err
}
@@ -253,11 +253,11 @@ func CreateSummitLogActivity(app core.App, ctx context.Context, summitLog *core.
mentions := []string{}
cc := pub.ItemCollection{pub.IRI(summitLogAuthor.GetString("followers"))}
mentionTags := pub.ItemCollection{}
for _, m := range mentionedActors {
for i, 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, fmt.Sprintf("@%s@%s", m.GetString("preferred_username"), m.GetString("domain"))))
mention.Name = pub.NaturalLanguageValuesNew(pub.LangRefValueNew(pub.NilLangRef, handles[i]))
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, recipient *core.Record, activity pub.Activity) error {
func ProcessCreateOrUpdateActivity(app core.App, actor *core.Record, activity pub.Activity) error {
var err error
if strings.Contains(activity.Object.GetID().String(), "/api/v1/trail") {
err = processCreateOrUpdateTrailActivity(activity, app, actor, recipient)
err = processCreateOrUpdateTrailActivity(activity, app, actor)
} 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, recipient)
err = processCreateOrUpdateListActivity(activity, app, actor)
} else {
err = processCreateOrUpdateCommentActivity(activity, app, actor)
}
@@ -465,16 +465,14 @@ func ProcessCreateOrUpdateActivity(app core.App, actor *core.Record, recipient *
}
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
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
}
_, err = util.InsertIntoFeed(app, recipient.Id, actor.Id, trail.Id, util.TrailFeed)
if err != nil {
return err
}
trail, err := util.TrailFromActivity(activity, app, actor)
trailObject, _ := pub.ToObject(activity.Object)
@@ -793,24 +791,22 @@ func processCreateOrUpdateSummitLogActivity(activity pub.Activity, app core.App,
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
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
}
_, err = util.InsertIntoFeed(app, recipient.Id, actor.Id, list.Id, util.ListFeed)
if err != nil {
return err
}
_, err := util.ListFromActivity(activity, app, actor)
return err
}
func ActorsFromMentions(app core.App, ctx context.Context, htmlStr string) ([]*core.Record, error) {
func ActorsFromMentions(app core.App, actor *core.Record, htmlStr string) ([]*core.Record, []string, error) {
doc, err := html.Parse(strings.NewReader(htmlStr))
if err != nil {
return nil, err
return nil, nil, err
}
var handles []string
@@ -842,12 +838,12 @@ func ActorsFromMentions(app core.App, ctx context.Context, htmlStr string) ([]*c
f(doc)
for _, h := range handles {
actor, err := GetActorByHandle(app, ctx, h, false)
actor, err := GetActorByHandle(app, actor, h, false)
if err != nil {
continue
}
actors = append(actors, actor)
}
return actors, nil
return actors, handles, nil
}

View File

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

View File

@@ -1,58 +1,64 @@
module pocketbase
go 1.25.0
go 1.23.0
toolchain go1.24.1
require (
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
github.com/meilisearch/meilisearch-go v0.29.0
github.com/pocketbase/dbx v1.11.0
github.com/pocketbase/pocketbase v0.26.1
)
require (
git.sr.ht/~mariusor/go-xsd-duration v0.0.0-20220703122237-02e73435a078 // indirect
github.com/aymerick/douceur v0.2.0 // 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/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/gorilla/css v1.0.1 // indirect
github.com/valyala/fastjson v1.6.10 // 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
)
require (
github.com/andybalholm/brotli v1.2.0 // indirect
github.com/andybalholm/brotli v1.1.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.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/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/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.22 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/microcosm-cc/bluemonday v1.0.27
github.com/ncruces/go-strftime v1.0.0 // indirect
github.com/ncruces/go-strftime v0.1.9 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/spf13/cast v1.10.0
github.com/spf13/cobra v1.10.2
github.com/spf13/pflag v1.0.10 // 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/twpayne/go-polyline v1.1.1
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
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
modernc.org/mathutil v1.7.1 // indirect
modernc.org/memory v1.11.0 // indirect
modernc.org/sqlite v1.50.0 // indirect
modernc.org/memory v1.8.2 // indirect
modernc.org/sqlite v1.36.1 // indirect
)

197
db/go.sum
View File

@@ -2,8 +2,12 @@ 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/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=
github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
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/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=
@@ -20,127 +24,130 @@ 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.19.0 h1:Zp3PiM21/9Ld6FzSKyL5c/BULoe/ONr9KlbYVOfG8+w=
github.com/fatih/color v1.19.0/go.mod h1:zNk67I0ZUT1bEGsSGyCZYZNrHuTkJJB+r6Q9VuMi0LE=
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/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
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/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/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.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/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/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
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/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/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/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/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
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.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/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/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 v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
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/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.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/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/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.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
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/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
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/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/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.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/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/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.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=
github.com/valyala/fastjson v1.6.4 h1:uAUNq9Z6ymTgGhcm0UynUAB6tlbakBrz6CQFax3BXVQ=
github.com/valyala/fastjson v1.6.4/go.mod h1:CLCAqky6SMuOcxStkYQvblddUtoRxhYMGLrsQns1aXY=
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.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI=
golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q=
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/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
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/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/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.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/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/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.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/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/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.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/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
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=
golang.org/x/tools v0.31.0 h1:0EedkvKDbh+qistFTd0Bcwe/YLh4vHwWEkiI0toFIBU=
golang.org/x/tools v0.31.0/go.mod h1:naFTU+Cev749tSJRXJlna0T3WxKvb1kWEx15xA4SdmQ=
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=
@@ -148,30 +155,26 @@ 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.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/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/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
modernc.org/memory v1.8.2 h1:cL9L4bcoAObu4NkxOlKWBWtNHIsnnACGF/TbqQ6sbcI=
modernc.org/memory v1.8.2/go.mod h1:ZbjSvMO5NQ1A2i3bWeDiVMxIorXwdClKE/0SZ+BMotU=
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.50.0 h1:eMowQSWLK0MeiQTdmz3lqoF5dqclujdlIKeJA11+7oM=
modernc.org/sqlite v1.50.0/go.mod h1:m0w8xhwYUVY3H6pSDwc3gkJ/irZT/0YEXwBlhaxQEew=
modernc.org/sqlite v1.36.1 h1:bDa8BJUH4lg6EGkLbahKe/8QqoF8p9gArSc6fTqYhyQ=
modernc.org/sqlite v1.36.1/go.mod h1:7MPwH7Z6bREicF9ZVUR78P1IKuxfZ8mRIDHD0iD+8TU=
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=

View File

@@ -1,22 +0,0 @@
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()
}
}

View File

@@ -1,47 +0,0 @@
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())
}
}

View File

@@ -1,65 +0,0 @@
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()
}
}

View File

@@ -1,57 +0,0 @@
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()
}
}

View File

@@ -1,24 +0,0 @@
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()
}
}

View File

@@ -1,146 +0,0 @@
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
}

View File

@@ -1,105 +0,0 @@
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()
}
}

View File

@@ -1,56 +0,0 @@
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()
}
}

View File

@@ -1,96 +0,0 @@
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
}
}

View File

@@ -1,118 +0,0 @@
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()
}
}

View File

@@ -1,57 +0,0 @@
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()
}
}

View File

@@ -1,122 +0,0 @@
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()
}
}

View File

@@ -1,101 +0,0 @@
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

@@ -1,927 +0,0 @@
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

@@ -1,209 +0,0 @@
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 (
"context"
"bytes"
"encoding/base64"
"encoding/json"
"errors"
@@ -13,18 +13,14 @@ 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/tkrajina/gpxgo/gpx"
"pocketbase/services/trailmerge"
"pocketbase/util"
"github.com/twpayne/go-gpx"
)
func SyncKomoot(app core.App, client meilisearch.ServiceManager) error {
func SyncKomoot(app core.App) error {
integrations, err := app.FindAllRecords("integrations", dbx.NewExp("true"))
if err != nil {
return err
@@ -44,17 +40,12 @@ func SyncKomoot(app core.App, client meilisearch.ServiceManager) error {
app.Logger().Warn(warning)
continue
}
ctx, err := util.GetSafeActorContext(nil, actor)
if err != nil {
continue
}
actorId := actor.Id
komootString := i.GetString("komoot")
komootIntegration := KomootIntegration{
Planned: true,
Completed: true,
Merge: trailmerge.DefaultIntegrationAutoMergeSettings(),
}
json.Unmarshal([]byte(komootString), &komootIntegration)
@@ -78,27 +69,25 @@ func SyncKomoot(app core.App, client meilisearch.ServiceManager) error {
app.Logger().Warn(warning)
continue
}
totalPages := 1
for page := 0; page < totalPages; page++ {
tours, tp, err := k.fetchTours(page)
hasNewTours := true
page := 0
for hasNewTours {
tours, err := k.fetchTours(page)
if err != nil {
warning := fmt.Sprintf("error fetching tours from komoot (page %d): %v\n", page, err)
warning := fmt.Sprintf("error fetching tours from komoot: %v\n", err)
fmt.Print(warning)
app.Logger().Warn(warning)
break
continue
}
totalPages = tp
allAlreadySynced, err := syncTrailWithTours(app, client, ctx, k, komootIntegration, userId, actor, tours)
hasNewTours, err = syncTrailWithTours(app, k, komootIntegration, userId, actorId, tours)
if err != nil {
warning := fmt.Sprintf("error syncing komoot tours with trails: %v\n", err)
fmt.Print(warning)
app.Logger().Warn(warning)
break
}
if allAlreadySynced {
break
continue
}
page += 1
}
}
@@ -168,18 +157,20 @@ func (k *KomootApi) Login(email, password string) error {
return nil
}
func (k *KomootApi) fetchTours(page int) ([]KomootTour, int, error) {
func (k *KomootApi) fetchTours(page int) ([]KomootTour, 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, 0, err
return nil, err
}
var data KomootToursResponse
json.Unmarshal(body, &data)
return data.Embedded.Tours, data.Page.TotalPages, nil
tours := data.Embedded.Tours
return tours, nil
}
func (k *KomootApi) fetchDetailedTour(tour KomootTour) (*DetailedKomootTour, error) {
@@ -194,25 +185,17 @@ func (k *KomootApi) fetchDetailedTour(tour KomootTour) (*DetailedKomootTour, err
return data, nil
}
// 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
func syncTrailWithTours(app core.App, k *KomootApi, i KomootIntegration, user string, actor string, tours []KomootTour) (bool, error) {
hasNewTours := false
for _, tour := range tours {
existingTrail, err := util.FindTrailByExternalReference(app, "komoot", strconv.Itoa(int(tour.ID)))
trails, err := app.FindRecordsByFilter("trails", "external_id = {:id}", "", 1, 0, dbx.Params{"id": strconv.Itoa(int(tour.ID))})
if err != nil {
return false, err
return hasNewTours, err
}
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) {
if len(trails) != 0 || (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))
@@ -223,30 +206,27 @@ func syncTrailWithTours(app core.App, client meilisearch.ServiceManager, ctx con
app.Logger().Warn(fmt.Sprintf("Unable to generate GPX for tour '%s': %v", tour.Name, err))
continue
}
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)
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
}
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))
err = createTrailFromTour(app, k, detailedTour, gpx, actor, wpIds)
if err != nil {
app.Logger().Warn(fmt.Sprintf("Unable to create trail for tour '%s': %v", tour.Name, err))
continue
}
}
return allAlreadySynced, nil
return hasNewTours, nil
}
func createTrailFromTour(app core.App, k *KomootApi, detailedTour *DetailedKomootTour, gpx *filesystem.File, user string, actor string, privacy string) (string, error) {
func createTrailFromTour(app core.App, k *KomootApi, detailedTour *DetailedKomootTour, gpx *filesystem.File, actor string, wpIds []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)
@@ -272,12 +252,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)
}
@@ -287,25 +267,10 @@ 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": public,
"completed": detailedTour.Type == "tour_recorded",
"public": detailedTour.Status == "public",
"distance": detailedTour.Distance,
"elevation_gain": detailedTour.ElevationUp,
"elevation_loss": detailedTour.ElevationDown,
@@ -317,6 +282,7 @@ func createTrailFromTour(app core.App, k *KomootApi, detailedTour *DetailedKomoo
"lon": detailedTour.StartPoint.Lng,
"difficulty": diffculty,
"category": categoryId,
"waypoints": wpIds,
"author": actor,
})
@@ -328,16 +294,13 @@ func createTrailFromTour(app core.App, k *KomootApi, detailedTour *DetailedKomoo
}
if err := app.Save(record); err != nil {
return "", err
}
if err := util.EnsureTrailExternalReference(app, trailid, "komoot", strconv.Itoa(detailedTour.ID)); err != nil {
return "", err
return err
}
if detailedTour.Type == "tour_recorded" {
collection, err := app.FindCollectionByNameOrId("summit_logs")
if err != nil {
return "", err
return err
}
summitLogRecord := core.NewRecord(collection)
@@ -351,23 +314,25 @@ func createTrailFromTour(app core.App, k *KomootApi, detailedTour *DetailedKomoo
"trail": trailid,
})
if err := app.Save(summitLogRecord); err != nil {
return "", err
return err
}
}
return trailid, nil
return nil
}
func createWaypointsFromTour(app core.App, tour *DetailedKomootTour, user string, trailid string) error {
func createWaypointsFromTour(app core.App, tour *DetailedKomootTour, user string) ([]string, error) {
collection, err := app.FindCollectionByNameOrId("waypoints")
if err != nil {
return err
return nil, err
}
for _, wp := range tour.Embedded.Timeline.Embedded.Items {
wpIds := make([]string, len(tour.Embedded.Timeline.Embedded.Items))
for i, wp := range tour.Embedded.Timeline.Embedded.Items {
photos, err := fetchWaypointPhotos(wp)
if err != nil {
return err
return nil, err
}
record := core.NewRecord(collection)
@@ -394,7 +359,6 @@ func createWaypointsFromTour(app core.App, tour *DetailedKomootTour, user string
"icon": "circle",
"author": user,
"distance_from_start": 0,
"trail": trailid,
})
if photos != nil {
@@ -402,11 +366,13 @@ func createWaypointsFromTour(app core.App, tour *DetailedKomootTour, user string
}
if err := app.Save(record); err != nil {
return err
return nil, err
}
wpIds[i] = record.Id
}
return nil
return wpIds, nil
}
func fetchRoutePhotos(k *KomootApi, tour *DetailedKomootTour) ([]*filesystem.File, error) {
@@ -422,9 +388,9 @@ func fetchRoutePhotos(k *KomootApi, tour *DetailedKomootTour) ([]*filesystem.Fil
return nil, err
}
photos := make([]*filesystem.File, 0, len(data.Embedded.Items))
photos := make([]*filesystem.File, data.Page.TotalElements)
for _, img := range data.Embedded.Items {
for i, img := range data.Embedded.Items {
photo, err := fetchPhoto(img.Src, "", "")
if err != nil {
return nil, err
@@ -432,7 +398,7 @@ func fetchRoutePhotos(k *KomootApi, tour *DetailedKomootTour) ([]*filesystem.Fil
if strings.HasSuffix(photo.Name, ".gif") {
continue
}
photos = append(photos, photo)
photos[i] = photo
//TODO: komoot photos can have location data. Maybe we should create a waypoint for those photos?
}
@@ -442,9 +408,9 @@ func fetchRoutePhotos(k *KomootApi, tour *DetailedKomootTour) ([]*filesystem.Fil
func fetchWaypointPhotos(wp Item) ([]*filesystem.File, error) {
photos := make([]*filesystem.File, 0, len(wp.Embedded.Reference.Embedded.Images.Embedded.Items))
photos := make([]*filesystem.File, len(wp.Embedded.Reference.Embedded.Images.Embedded.Items))
for _, img := range wp.Embedded.Reference.Embedded.Images.Embedded.Items {
for i, img := range wp.Embedded.Reference.Embedded.Images.Embedded.Items {
photo, err := fetchPhoto(img.Src, "", "")
if err != nil {
return nil, err
@@ -452,7 +418,7 @@ func fetchWaypointPhotos(wp Item) ([]*filesystem.File, error) {
if strings.HasSuffix(photo.Name, ".gif") {
continue
}
photos = append(photos, photo)
photos[i] = photo
}
return photos, nil
@@ -472,36 +438,35 @@ func fetchPhoto(url string, width string, height string) (*filesystem.File, erro
}
func generateTourGPX(detailedTour *DetailedKomootTour) (*filesystem.File, error) {
var points []gpx.GPXPoint
var points []*gpx.WptType
for _, item := range detailedTour.Embedded.Coordinates.Items {
t := detailedTour.Date.Unix() + int64(item.T/1000)
points = append(points, gpx.GPXPoint{
Point: gpx.Point{Latitude: item.Lat, Longitude: item.Lng, Elevation: *gpx.NewNullableFloat64(item.Alt)},
Timestamp: time.Unix(t, 0)})
points = append(points, &gpx.WptType{Lat: item.Lat, Lon: item.Lng, Ele: item.Alt, Time: time.Unix(t, 0)})
}
gpxData := &gpx.GPX{
gpx := &gpx.GPX{
Version: "1.1",
Creator: "komoot GPX Exporter",
Tracks: []gpx.GPXTrack{
Trk: []*gpx.TrkType{
{
Name: detailedTour.Name,
Segments: []gpx.GPXTrackSegment{
TrkSeg: []*gpx.TrkSegType{
{
Points: points,
TrkPt: points,
},
},
},
},
}
gpxAsXML, err := gpxData.ToXml(gpx.ToXmlParams{Version: "1.1", Indent: true})
var buf bytes.Buffer
err := gpx.Write(&buf)
if err != nil {
return nil, err
}
gpxFile, err := filesystem.NewFileFromBytes(gpxAsXML, detailedTour.Name+".gpx")
gpxFile, err := filesystem.NewFileFromBytes(buf.Bytes(), detailedTour.Name+".gpx")
if err != nil {
return nil, err
}

View File

@@ -1,19 +1,13 @@
package komoot
import (
"time"
"pocketbase/services/trailmerge"
)
import "time"
type KomootIntegration struct {
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"`
Active bool `json:"active"`
Email string `json:"email"`
Password string `json:"password"`
Planned bool `json:"planned"`
Completed bool `json:"completed"`
}
type LoginResponse struct {

View File

@@ -1,10 +1,6 @@
package strava
import (
"time"
"pocketbase/services/trailmerge"
)
import "time"
type TokenRequest struct {
ClientID int32 `json:"client_id"`
@@ -25,17 +21,14 @@ 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"`
Privacy string `json:"privacy"`
After string `json:"after,omitempty"`
Merge trailmerge.IntegrationAutoMergeSettings `json:"merge"`
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"`
}
type StravaRoute struct {
Athlete Athlete `json:"athlete"`

View File

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

1340
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(&[]interface{}{
_, err = client.Index("trails").UpdateFilterableAttributes(&[]string{
"_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(&[]interface{}{
_, err = client.Index("lists").UpdateFilterableAttributes(&[]string{
"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(&[]any{
_, err = client.Index("trails").UpdateFilterableAttributes(&[]string{
"_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(&[]any{
_, err = client.Index("trails").UpdateFilterableAttributes(&[]string{
"_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

@@ -1,115 +0,0 @@
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

@@ -1,46 +0,0 @@
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

@@ -1,40 +0,0 @@
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

@@ -1,100 +0,0 @@
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

@@ -1,106 +0,0 @@
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

@@ -1,57 +0,0 @@
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

@@ -1,44 +0,0 @@
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

@@ -1,216 +0,0 @@
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

@@ -1,40 +0,0 @@
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

@@ -1,40 +0,0 @@
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

@@ -1,42 +0,0 @@
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

@@ -1,116 +0,0 @@
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

@@ -1,42 +0,0 @@
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

@@ -1,48 +0,0 @@
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

@@ -1,42 +0,0 @@
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

@@ -1,42 +0,0 @@
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

@@ -1,42 +0,0 @@
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

@@ -1,78 +0,0 @@
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

@@ -1,42 +0,0 @@
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

@@ -1,42 +0,0 @@
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

@@ -1,62 +0,0 @@
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

@@ -1,34 +0,0 @@
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

@@ -1,44 +0,0 @@
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

@@ -1,46 +0,0 @@
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

@@ -1,42 +0,0 @@
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

@@ -1,41 +0,0 @@
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

@@ -1,41 +0,0 @@
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

@@ -1,61 +0,0 @@
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

@@ -1,60 +0,0 @@
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

@@ -1,38 +0,0 @@
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

@@ -1,120 +0,0 @@
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

@@ -1,81 +0,0 @@
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

@@ -1,83 +0,0 @@
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

@@ -1,45 +0,0 @@
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

@@ -1,138 +0,0 @@
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

@@ -1,178 +0,0 @@
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

@@ -1,62 +0,0 @@
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

@@ -1,60 +0,0 @@
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

@@ -1,78 +0,0 @@
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

@@ -1,33 +0,0 @@
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

@@ -1,44 +0,0 @@
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

@@ -1,40 +0,0 @@
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

@@ -1,42 +0,0 @@
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

@@ -1,42 +0,0 @@
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

@@ -1,62 +0,0 @@
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

@@ -1,88 +0,0 @@
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

@@ -1,40 +0,0 @@
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

@@ -1,42 +0,0 @@
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

@@ -1,49 +0,0 @@
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

@@ -1,82 +0,0 @@
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

@@ -1,82 +0,0 @@
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