Compare commits
425 Commits
brian/trai
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
51bed0d3f3 | ||
|
|
bfa8dfefbd | ||
|
|
7b7b841659 | ||
|
|
f69ad01029 | ||
|
|
611a7b5f02 | ||
|
|
0bb55ef84e | ||
|
|
c9024a4322 | ||
|
|
ea6f5d5c95 | ||
|
|
b2753c29d2 | ||
|
|
868010459f | ||
|
|
be8de33c4d | ||
|
|
677d3a12b5 | ||
|
|
1b2781960a | ||
|
|
edad99c7b4 | ||
|
|
d2ac49470a | ||
|
|
992ebfc0c4 | ||
|
|
b5739cb72e | ||
|
|
d2268429c7 | ||
|
|
f1e2ae4088 | ||
|
|
17343bac9e | ||
|
|
817616e801 | ||
|
|
54e2fc2fe6 | ||
|
|
a11a4cb3cb | ||
|
|
127b7a0745 | ||
|
|
25fe7982c5 | ||
|
|
5cc6f7bd55 | ||
|
|
798970daa3 | ||
|
|
6d1d1236ef | ||
|
|
85e5fb6df7 | ||
|
|
7bdf0d3151 | ||
|
|
7e84e39cda | ||
|
|
86c94a5ce2 | ||
|
|
35a5d657bf | ||
|
|
41ee45d0e4 | ||
|
|
75a7e08433 | ||
|
|
345452e720 | ||
|
|
c9256aeb29 | ||
|
|
5457057bb2 | ||
|
|
25c63ffa56 | ||
|
|
19fd259129 | ||
|
|
871bf38112 | ||
|
|
1f17a8784b | ||
|
|
b7b16e5a6e | ||
|
|
da38cfc19a | ||
|
|
814009c463 | ||
|
|
9b3afbface | ||
|
|
bef362914f | ||
|
|
5279ab4279 | ||
|
|
012a416862 | ||
|
|
1ca1431207 | ||
|
|
b2e188f845 | ||
|
|
7375f03818 | ||
|
|
f9bf5372d5 | ||
|
|
f6354dcd56 | ||
|
|
591a72e2d1 | ||
|
|
812bf5f626 | ||
|
|
2430bfe671 | ||
|
|
fa5234a390 | ||
|
|
ec76b69dba | ||
|
|
04818b9329 | ||
|
|
369e71fcec | ||
|
|
5a05573e83 | ||
|
|
83a64775d0 | ||
|
|
0623287206 | ||
|
|
debb1bee31 | ||
|
|
4b1dc784c9 | ||
|
|
c9acf8ad91 | ||
|
|
711a592038 | ||
|
|
ba88d08078 | ||
|
|
039650b950 | ||
|
|
71a5ddadc8 | ||
|
|
143fa8e73e | ||
|
|
0c24173f3f | ||
|
|
6cd7a20bad | ||
|
|
84bf27a415 | ||
|
|
dd77f21d6a | ||
|
|
88aabfa6fd | ||
|
|
f2396ae574 | ||
|
|
2b9d5305a8 | ||
|
|
3d862c64e8 | ||
|
|
bebfb08e1c | ||
|
|
a00070a9eb | ||
|
|
cde1f73288 | ||
|
|
bd293e6739 | ||
|
|
230f42d639 | ||
|
|
536276e0b3 | ||
|
|
a0eaedd156 | ||
|
|
31171e15f6 | ||
|
|
6c9b41342a | ||
|
|
21fe188fdc | ||
|
|
6cac905368 | ||
|
|
bb8b4fbc2e | ||
|
|
573eb9df1f | ||
|
|
34039f3d43 | ||
|
|
702681acee | ||
|
|
23b0913c9a | ||
|
|
623bc7b036 | ||
|
|
70ffac6902 | ||
|
|
9973540fce | ||
|
|
f6091ff01b | ||
|
|
be8465be78 | ||
|
|
b09b7d51a2 | ||
|
|
088e09dc53 | ||
|
|
ff10084ba0 | ||
|
|
c8af897cdd | ||
|
|
5595f9b4d3 | ||
|
|
01d85b32f3 | ||
|
|
17a0f5cce8 | ||
|
|
ae317afdbe | ||
|
|
0368892014 | ||
|
|
0c715ad5ba | ||
|
|
adc32339b6 | ||
|
|
6789a666ef | ||
|
|
67b80f01db | ||
|
|
bdd83b10c7 | ||
|
|
3a541d4d9a | ||
|
|
840032e610 | ||
|
|
c0be0629e3 | ||
|
|
47672c5a9d | ||
|
|
721c84c1d8 | ||
|
|
98389d774d | ||
|
|
5a0118c175 | ||
|
|
687f474900 | ||
|
|
12713568e4 | ||
|
|
677f2bc473 | ||
|
|
d74f43ddc6 | ||
|
|
46a12647ee | ||
|
|
f877e0b266 | ||
|
|
d1c87577de | ||
|
|
82ad995cdc | ||
|
|
301377527f | ||
|
|
32a9a67e7e | ||
|
|
df55864a6c | ||
|
|
7643bd11e4 | ||
|
|
a4fcbb21fd | ||
|
|
7c7c5f89fb | ||
|
|
7c5d6012dd | ||
|
|
f864b55019 | ||
|
|
f82e064ce8 | ||
|
|
357b446bfd | ||
|
|
7224e41930 | ||
|
|
77dcac7808 | ||
|
|
359957017f | ||
|
|
32e6b6a6cc | ||
|
|
44fbaa5e27 | ||
|
|
9db829a09f | ||
|
|
27728c4ce7 | ||
|
|
f99c650f8b | ||
|
|
dacdaeab26 | ||
|
|
6d5494935d | ||
|
|
029f1f134b | ||
|
|
1005aa0a84 | ||
|
|
6318f4b4ad | ||
|
|
7546abb122 | ||
|
|
09c7f26db9 | ||
|
|
e685bc2e2a | ||
|
|
b435e02cb9 | ||
|
|
4b749c9868 | ||
|
|
c2b86f5521 | ||
|
|
b65fbdd128 | ||
|
|
c80dc01c7a | ||
|
|
57b37d84c4 | ||
|
|
839679074f | ||
|
|
e1764c128e | ||
|
|
7fb82216fe | ||
|
|
4eae263be1 | ||
|
|
9a50569bb3 | ||
|
|
86cca8e8c3 | ||
|
|
6e5edf7ed7 | ||
|
|
9282f37c15 | ||
|
|
741cb51096 | ||
|
|
712174491b | ||
|
|
555ecfdc5e | ||
|
|
81d148c09f | ||
|
|
237225f10b | ||
|
|
84d9f2483e | ||
|
|
8bf4138962 | ||
|
|
b09675f2b2 | ||
|
|
7ed0b98fce | ||
|
|
44bb49ec96 | ||
|
|
23dd744b28 | ||
|
|
03ef547a75 | ||
|
|
d65d812d4d | ||
|
|
e910b3c44d | ||
|
|
2edba0549a | ||
|
|
722bd6c078 | ||
|
|
4010b85442 | ||
|
|
3ced9e0dd6 | ||
|
|
e6f47b348c | ||
|
|
1632bc4f34 | ||
|
|
149157604d | ||
|
|
cf74a711e9 | ||
|
|
5580832675 | ||
|
|
411bb3cc45 | ||
|
|
378f030b4a | ||
|
|
97abc9f3d0 | ||
|
|
2cb02c0236 | ||
|
|
88406ee175 | ||
|
|
72b57f85bd | ||
|
|
e9422efc5d | ||
|
|
fbd61774dc | ||
|
|
678dacb1a1 | ||
|
|
d736e3b500 | ||
|
|
f627341f1c | ||
|
|
cef7c3ac21 | ||
|
|
4d6b557ed0 | ||
|
|
12ea2addfb | ||
|
|
4c6990e7a4 | ||
|
|
3d37d51b0b | ||
|
|
b4ff4795bc | ||
|
|
ca477984cc | ||
|
|
adf41c370b | ||
|
|
72acac072c | ||
|
|
962fddc1e2 | ||
|
|
24908993fa | ||
|
|
b2be48a282 | ||
|
|
73d20b1cbc | ||
|
|
9c628c6511 | ||
|
|
3b7f30908c | ||
|
|
8cc7029243 | ||
|
|
5174663104 | ||
|
|
64579808a9 | ||
|
|
c6042db9a8 | ||
|
|
16458dc365 | ||
|
|
b734c325ee | ||
|
|
64320ab31c | ||
|
|
9dfbd106af | ||
|
|
625c5e302d | ||
|
|
5dcc7d057b | ||
|
|
39f33f1eb2 | ||
|
|
4ffd2dd0c2 | ||
|
|
7807c5c7c2 | ||
|
|
66e482c6fc | ||
|
|
0032556cb9 | ||
|
|
9597435dff | ||
|
|
432176d8f5 | ||
|
|
6b0bdcfff6 | ||
|
|
c867fd7319 | ||
|
|
c0116ebbec | ||
|
|
1f42b6c85b | ||
|
|
a8de5d075f | ||
|
|
a00d92e93b | ||
|
|
7596a916f8 | ||
|
|
7f82509f3d | ||
|
|
2502836d13 | ||
|
|
00bb717def | ||
|
|
7cadd08cb0 | ||
|
|
a02c6698c5 | ||
|
|
98d3a90546 | ||
|
|
c904e3d34e | ||
|
|
4ba0281e0f | ||
|
|
5205d721a0 | ||
|
|
2aad94058a | ||
|
|
089ce933dd | ||
|
|
056805f6a2 | ||
|
|
f73a109866 | ||
|
|
c93ab89f51 | ||
|
|
1cc70c4e4b | ||
|
|
47f431e18d | ||
|
|
05ad5ce8e0 | ||
|
|
fdb6ef3b76 | ||
|
|
380c1802f2 | ||
|
|
33776ad42c | ||
|
|
e375747422 | ||
|
|
f9451d73bf | ||
|
|
1e22c78c22 | ||
|
|
b3d44f3b78 | ||
|
|
bdfaedb5fd | ||
|
|
097ecd409d | ||
|
|
e3916e371a | ||
|
|
c47654cfa2 | ||
|
|
b1173d52ae | ||
|
|
ef4bc9c961 | ||
|
|
482d90c893 | ||
|
|
b7c06fcaf2 | ||
|
|
53508be008 | ||
|
|
1c10e90db7 | ||
|
|
396e64b5a6 | ||
|
|
46d3c45b58 | ||
|
|
353e52b0f6 | ||
|
|
634c34ce65 | ||
|
|
731a4ffb97 | ||
|
|
ef1571c54e | ||
|
|
6a908a3ace | ||
|
|
f46f8055c4 | ||
|
|
eac9b2e489 | ||
|
|
baa7bb4ecf | ||
|
|
687fc0e1f6 | ||
|
|
03bf5aa5ad | ||
|
|
e8c7e7721a | ||
|
|
237d12bfec | ||
|
|
25bf00a38b | ||
|
|
2aaa3537a2 | ||
|
|
620d724972 | ||
|
|
efeafe7222 | ||
|
|
783e2cf233 | ||
|
|
dc7afffe8a | ||
|
|
7cfca84ee3 | ||
|
|
d21df88be2 | ||
|
|
68f36dbf0c | ||
|
|
d6e08e6d80 | ||
|
|
d577ac209e | ||
|
|
95377f03c3 | ||
|
|
612176b1ad | ||
|
|
c8dc4135a6 | ||
|
|
7791acb1fe | ||
|
|
6035c3933a | ||
|
|
191a5f3342 | ||
|
|
92e4a3c75e | ||
|
|
b96da13834 | ||
|
|
f15633177a | ||
|
|
f313cded51 | ||
|
|
e8a356c52d | ||
|
|
9585d9ec1f | ||
|
|
86c40d8319 | ||
|
|
24448b2f32 | ||
|
|
7c0685d887 | ||
|
|
52302bc517 | ||
|
|
da41413394 | ||
|
|
f0b58e7949 | ||
|
|
b7ebff3586 | ||
|
|
26d1e2728d | ||
|
|
1cd99c4980 | ||
|
|
e0f25137c8 | ||
|
|
3a9f52d055 | ||
|
|
93082f3cdc | ||
|
|
adb9045acb | ||
|
|
82c95a4896 | ||
|
|
ce9c737ca6 | ||
|
|
247f02c2fc | ||
|
|
e923ec45a4 | ||
|
|
a7194027d8 | ||
|
|
7c478f6f3f | ||
|
|
819aea801e | ||
|
|
fff3bd1bf7 | ||
|
|
57234acf3a | ||
|
|
0121208f46 | ||
|
|
ce14d013b0 | ||
|
|
3d978c87bf | ||
|
|
0166943720 | ||
|
|
94a6915d6b | ||
|
|
7f1b34e66c | ||
|
|
3d23ae64a1 | ||
|
|
8bc469fbc3 | ||
|
|
783bde9e07 | ||
|
|
8ad9eb5b73 | ||
|
|
6cb91f5b48 | ||
|
|
70a7e45235 | ||
|
|
a26faff6ed | ||
|
|
4f941817ff | ||
|
|
34653657f9 | ||
|
|
8175e2566a | ||
|
|
4af631d424 | ||
|
|
efebcc176b | ||
|
|
bc2fc68394 | ||
|
|
67aa566888 | ||
|
|
39ed56ecb4 | ||
|
|
4fa058b032 | ||
|
|
1ec7032f1e | ||
|
|
625b6e153f | ||
|
|
e69f02f965 | ||
|
|
5014193ac7 | ||
|
|
d1481e1d72 | ||
|
|
ed9ab91fa8 | ||
|
|
f6033e7a03 | ||
|
|
9d3555ee84 | ||
|
|
c60e15e228 | ||
|
|
d9c3cbe605 | ||
|
|
80911faa41 | ||
|
|
78406e3130 | ||
|
|
753c1f1385 | ||
|
|
7b57252f7f | ||
|
|
277b435c65 | ||
|
|
65c04781cc | ||
|
|
9fb38abda3 | ||
|
|
691418df5d | ||
|
|
9c37c19188 | ||
|
|
d860481723 | ||
|
|
d807eaaf9b | ||
|
|
7ab8c843e2 | ||
|
|
2902f4f07a | ||
|
|
b86e735bac | ||
|
|
9c497ea2e4 | ||
|
|
f2badbeb12 | ||
|
|
99f852b74c | ||
|
|
06f6d58b5e | ||
|
|
e70d136ad9 | ||
|
|
0c6fdc21bf | ||
|
|
00e64ff57f | ||
|
|
c6aa1e1800 | ||
|
|
fe9f47a228 | ||
|
|
79b7ddd4f5 | ||
|
|
ccd9f64f48 | ||
|
|
0e1c799ef5 | ||
|
|
2694589d90 | ||
|
|
63a7ec5a92 | ||
|
|
7803d9fc1b | ||
|
|
9b6522ec21 | ||
|
|
47e113efa2 | ||
|
|
13e047f94e | ||
|
|
eea73497fc | ||
|
|
6f0a92a474 | ||
|
|
5160918078 | ||
|
|
3961fc0c49 | ||
|
|
73544a54b5 | ||
|
|
52b6c7b58c | ||
|
|
1703fe1c23 | ||
|
|
e11354b976 | ||
|
|
642cc36326 | ||
|
|
a8e8343e6d | ||
|
|
797e31d1e6 | ||
|
|
2f30a3a3f7 | ||
|
|
fd50b06c7f | ||
|
|
c85f83f56e | ||
|
|
80e2222d4a | ||
|
|
7768fd17bf | ||
|
|
3adadfb365 | ||
|
|
67f542b40e | ||
|
|
39eedcd848 | ||
|
|
16e6d4df83 | ||
|
|
d8f3cb1abd | ||
|
|
450b34b4d6 | ||
|
|
79ab48596b | ||
|
|
b4ef7d8296 | ||
|
|
3baf47d864 |
24
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
24
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
name: "🐛 Bug Report"
|
||||
description: File a bug report.
|
||||
labels: ["bug"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thanks for taking the time to fill out this bug report!
|
||||
- type: textarea
|
||||
id: what-happened
|
||||
attributes:
|
||||
label: What happened?
|
||||
description: Also, please tell us what you expected to happen. Please convert .gpx files to .txt and attach them when appropriate.
|
||||
placeholder: Tell us what you see!
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: version
|
||||
attributes:
|
||||
label: Version
|
||||
description: What version of wanderer are you running?
|
||||
placeholder: ex. v0.17.2
|
||||
validations:
|
||||
required: true
|
||||
8
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
8
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: "💡 Feature request"
|
||||
url: https://github.com/open-wanderer/wanderer/discussions/new?category=ideas
|
||||
about: Suggest an idea for this project.
|
||||
- name: "❓ Ask a question"
|
||||
url: https://github.com/open-wanderer/wanderer/discussions/new?category=q-a
|
||||
about: Please ask and answer questions here.
|
||||
83
.github/dependabot.yml
vendored
Normal file
83
.github/dependabot.yml
vendored
Normal file
@@ -0,0 +1,83 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: gomod
|
||||
open-pull-requests-limit: 10
|
||||
directory: "/db"
|
||||
schedule:
|
||||
interval: "monthly"
|
||||
groups:
|
||||
gomod-backward-compatible:
|
||||
update-types:
|
||||
- minor
|
||||
- patch
|
||||
cooldown:
|
||||
default-days: 7
|
||||
- package-ecosystem: github-actions
|
||||
open-pull-requests-limit: 5
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "monthly"
|
||||
groups:
|
||||
github-actions-backward-compatible:
|
||||
update-types:
|
||||
- minor
|
||||
- patch
|
||||
cooldown:
|
||||
default-days: 7
|
||||
- package-ecosystem: npm
|
||||
open-pull-requests-limit: 5
|
||||
directory: "/web"
|
||||
schedule:
|
||||
interval: "monthly"
|
||||
groups:
|
||||
npm-backward-compatible:
|
||||
update-types:
|
||||
- minor
|
||||
- patch
|
||||
cooldown:
|
||||
default-days: 7
|
||||
- package-ecosystem: npm
|
||||
open-pull-requests-limit: 5
|
||||
directory: "/docs"
|
||||
schedule:
|
||||
interval: "monthly"
|
||||
groups:
|
||||
docker-backward-compatible:
|
||||
update-types:
|
||||
- minor
|
||||
- patch
|
||||
cooldown:
|
||||
default-days: 7
|
||||
- package-ecosystem: "docker"
|
||||
directory: "/docs"
|
||||
schedule:
|
||||
interval: "monthly"
|
||||
groups:
|
||||
docker-docs-backward-compatible:
|
||||
update-types:
|
||||
- minor
|
||||
- patch
|
||||
cooldown:
|
||||
default-days: 7
|
||||
- package-ecosystem: "docker"
|
||||
directory: "/search"
|
||||
schedule:
|
||||
interval: "monthly"
|
||||
groups:
|
||||
docker-search-backward-compatible:
|
||||
update-types:
|
||||
- minor
|
||||
- patch
|
||||
cooldown:
|
||||
default-days: 7
|
||||
- package-ecosystem: "docker"
|
||||
directory: "/web"
|
||||
schedule:
|
||||
interval: "monthly"
|
||||
groups:
|
||||
docker-web-backward-compatible:
|
||||
update-types:
|
||||
- minor
|
||||
- patch
|
||||
cooldown:
|
||||
default-days: 7
|
||||
33
.github/workflows/go.yml
vendored
Normal file
33
.github/workflows/go.yml
vendored
Normal file
@@ -0,0 +1,33 @@
|
||||
name: Go
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main ]
|
||||
paths:
|
||||
- '.github/**'
|
||||
- 'db/**'
|
||||
pull_request:
|
||||
paths:
|
||||
- '.github/**'
|
||||
- 'db/**'
|
||||
|
||||
jobs:
|
||||
db-test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: '1.25'
|
||||
|
||||
- run: make db-fmt
|
||||
- name: Ensure formatting
|
||||
run: |
|
||||
if [ -n "$(gofmt -l .)" ]; then
|
||||
echo "Go files are not formatted"
|
||||
exit 1
|
||||
fi
|
||||
git diff --exit-code
|
||||
working-directory: db
|
||||
- run: make db-vet
|
||||
- run: make db-test
|
||||
142
.github/workflows/release.yaml
vendored
142
.github/workflows/release.yaml
vendored
@@ -1,139 +1,97 @@
|
||||
name: Release Workflow
|
||||
name: Publish Release
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: "The version to release (e.g., 0.12.0)"
|
||||
required: true
|
||||
pull_request:
|
||||
types: [closed]
|
||||
branches: [main]
|
||||
|
||||
jobs:
|
||||
# Job 1: Bump Versions and Create Tags
|
||||
versioning:
|
||||
# Only run if the PR was merged AND it came from a release branch
|
||||
publish:
|
||||
if: github.event.pull_request.merged == true && startsWith(github.head_ref, 'release/v')
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
outputs:
|
||||
version: ${{ steps.set_version.outputs.version }}
|
||||
version: ${{ steps.get_version.outputs.version }}
|
||||
|
||||
steps:
|
||||
# 1. Checkout the repository
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v6
|
||||
|
||||
# 2. Setup node & npm
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: '22'
|
||||
|
||||
# 3. Bump versions using npm
|
||||
- name: Bump version in web and docs
|
||||
id: set_version
|
||||
run: |
|
||||
VERSION=${{ github.event.inputs.version }}
|
||||
cd web && npm version $VERSION --no-git-tag-version && cd ..
|
||||
cd docs && npm version $VERSION --no-git-tag-version && cd ..
|
||||
echo "version=v$VERSION" >> $GITHUB_OUTPUT
|
||||
|
||||
# 4. Tag and push the new versions
|
||||
- name: Commit and Tag
|
||||
- name: Set Version Output
|
||||
id: get_version
|
||||
run: |
|
||||
# Extract v0.12.0 from release/v0.12.0
|
||||
VERSION_TAG=${GITHUB_HEAD_REF#release/}
|
||||
echo "version=$VERSION_TAG" >> $GITHUB_OUTPUT
|
||||
|
||||
# Git Tagging
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||
git add web/package.json docs/package.json
|
||||
git commit -m "Release ${{ github.event.inputs.version }}"
|
||||
git tag "v${{ github.event.inputs.version }}"
|
||||
git push origin main --tags
|
||||
git tag $VERSION_TAG
|
||||
git push origin $VERSION_TAG
|
||||
|
||||
# Job 2: Build Docker Images
|
||||
docker-build:
|
||||
needs: publish
|
||||
runs-on: ubuntu-latest
|
||||
needs: versioning
|
||||
|
||||
steps:
|
||||
# 1. Checkout the repository
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
ref: ${{ github.ref }}
|
||||
|
||||
- uses: actions/checkout@v6
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v4
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: '1.22'
|
||||
|
||||
# 2. Log in to Docker Hub
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: '22'
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
# 3. Setup docker multi platform builds
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
uses: docker/setup-buildx-action@v4
|
||||
|
||||
# 4. Build and push Docker images
|
||||
- name: Build Docker Images
|
||||
env:
|
||||
VERSION: ${{ needs.versioning.outputs.version }}
|
||||
VERSION: ${{ needs.publish.outputs.version }}
|
||||
run: |
|
||||
# Build db image
|
||||
# cd db
|
||||
# env GOOS=linux GOARCH=arm64 go build -o pocketbase_arm64
|
||||
# env GOOS=linux GOARCH=amd64 go build -o pocketbase_amd64
|
||||
# cd ..
|
||||
# docker buildx build db/ --no-cache -t flomp/wanderer-db:$VERSION -t flomp/wanderer-db:latest --platform=linux/amd64,linux/arm64 --push
|
||||
|
||||
# Build web image
|
||||
export PUBLIC_VALHALLA_URL=https://valhalla.openstreetmap.de
|
||||
cd web
|
||||
npm ci && npm run build
|
||||
cd ..
|
||||
docker buildx build db/ --no-cache -t flomp/wanderer-db:$VERSION -t flomp/wanderer-db:latest --platform=linux/amd64,linux/arm64 --push
|
||||
npm --prefix web ci
|
||||
npm --prefix web run openapi:generate
|
||||
docker buildx build web/ --no-cache -t flomp/wanderer-web:$VERSION -t flomp/wanderer-web:latest --platform=linux/amd64,linux/arm64 --push
|
||||
npm --prefix docs ci
|
||||
npm --prefix docs run build
|
||||
docker buildx build ./docs --no-cache -t flomp/wanderer-docs:$VERSION -t flomp/wanderer-docs:latest --platform=linux/amd64,linux/arm64 --push
|
||||
|
||||
# Build docs image
|
||||
cd docs
|
||||
npm ci && npm run build
|
||||
cd ..
|
||||
docker buildx build docs/ --no-cache -t flomp/wanderer-docs:$VERSION -t flomp/wanderer-docs:latest --platform=linux/amd64,linux/arm64 --push
|
||||
|
||||
# Job 3: Publish the Release
|
||||
release:
|
||||
needs: [publish, docker-build]
|
||||
runs-on: ubuntu-latest
|
||||
needs: [versioning, docker-build]
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
# 1. Checkout the repository
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
ref: ${{ github.ref }}
|
||||
# 2. Extract release notes from CHANGELOG.md
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Extract release notes
|
||||
id: changelog
|
||||
run: |
|
||||
VERSION="${{ needs.versioning.outputs.version }}"
|
||||
CHANGELOG=$(awk -v ver="$VERSION" '
|
||||
BEGIN { in_section = 0 }
|
||||
/^# / {
|
||||
if (in_section) exit
|
||||
if ($2 == ver) in_section = 1
|
||||
}
|
||||
in_section { print }
|
||||
' CHANGELOG.md)
|
||||
VERSION="${{ needs.publish.outputs.version }}"
|
||||
# Clean 'v' from v0.12.0 for awk if your changelog uses 0.12.0
|
||||
RAW_VER=${VERSION#v}
|
||||
CHANGELOG=$(awk -v ver="$RAW_VER" 'BEGIN {in_section=0} /^# / {if (in_section) exit; if ($2 == ver) in_section=1} in_section {print}' CHANGELOG.md)
|
||||
echo 'changelog<<EOF' >> $GITHUB_OUTPUT
|
||||
printf "%s\n" "$CHANGELOG" >> $GITHUB_OUTPUT
|
||||
printf "$CHANGELOG" >> $GITHUB_OUTPUT
|
||||
echo 'EOF' >> $GITHUB_OUTPUT
|
||||
|
||||
# 3. Create GitHub Release
|
||||
- name: Create GitHub Release
|
||||
uses: actions/create-release@v1
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
tag_name: ${{ needs.versioning.outputs.version }}
|
||||
release_name: "${{ needs.versioning.outputs.version }}"
|
||||
tag_name: ${{ needs.publish.outputs.version }}
|
||||
body: ${{ steps.changelog.outputs.changelog }}
|
||||
draft: false
|
||||
prerelease: false
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
46
.github/workflows/release_dev.yaml
vendored
Normal file
46
.github/workflows/release_dev.yaml
vendored
Normal file
@@ -0,0 +1,46 @@
|
||||
name: Publish dev Release
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
docker-build:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: '22'
|
||||
|
||||
- name: Bump versions
|
||||
run: |
|
||||
LAST_TAG=$(git describe --tags --abbrev=0)
|
||||
BASE_VERSION=${LAST_TAG#v}
|
||||
FULL_SHA=$(git rev-parse HEAD)
|
||||
VERSION="${BASE_VERSION}-dev+${FULL_SHA}"
|
||||
cd web && npm version $VERSION --no-git-tag-version && cd ..
|
||||
cd docs && npm version $VERSION --no-git-tag-version && cd ..
|
||||
|
||||
- name: Login to GHCR
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v4
|
||||
|
||||
- name: Build Docker Images
|
||||
run: |
|
||||
docker buildx build db/ --no-cache -t ghcr.io/open-wanderer/wanderer-db:dev --push
|
||||
docker buildx build web/ --no-cache -t ghcr.io/open-wanderer/wanderer-web:dev --push
|
||||
39
.github/workflows/release_request.yaml
vendored
Normal file
39
.github/workflows/release_request.yaml
vendored
Normal file
@@ -0,0 +1,39 @@
|
||||
name: Release Request
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: "The version to release (e.g., 0.12.0)"
|
||||
required: true
|
||||
|
||||
jobs:
|
||||
create-pr:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: '22'
|
||||
|
||||
- name: Bump versions
|
||||
run: |
|
||||
VERSION=${{ github.event.inputs.version }}
|
||||
cd web && npm version $VERSION --no-git-tag-version && cd ..
|
||||
cd docs && npm version $VERSION --no-git-tag-version && cd ..
|
||||
|
||||
- name: Create Pull Request
|
||||
uses: peter-evans/create-pull-request@v8
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
commit-message: "chore: release v${{ github.event.inputs.version }}"
|
||||
branch: "release/v${{ github.event.inputs.version }}"
|
||||
title: "Release v${{ github.event.inputs.version }}"
|
||||
body: "This PR bumps the version. Merging this will trigger Docker builds and a GitHub Release."
|
||||
base: main
|
||||
38
.github/workflows/web.yaml
vendored
Normal file
38
.github/workflows/web.yaml
vendored
Normal file
@@ -0,0 +1,38 @@
|
||||
name: Web CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main ]
|
||||
paths:
|
||||
- '.github/**'
|
||||
- 'web/**'
|
||||
pull_request:
|
||||
paths:
|
||||
- '.github/**'
|
||||
- 'web/**'
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: '22'
|
||||
|
||||
- uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: '1.25'
|
||||
|
||||
- run: make db-build-docker
|
||||
|
||||
- run: make web-build-docker
|
||||
|
||||
- run: docker compose up -d
|
||||
|
||||
- run: make web-install
|
||||
- run: make web-playwright-install
|
||||
- run: make web-check
|
||||
- run: make web-test
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -9,6 +9,6 @@ search/dumps
|
||||
|
||||
run.sh
|
||||
build*.sh
|
||||
start.*
|
||||
start*.*
|
||||
|
||||
data*/
|
||||
257
CHANGELOG.md
257
CHANGELOG.md
@@ -1,10 +1,227 @@
|
||||
# v0.19.1
|
||||
|
||||
## Features
|
||||
- Speed improvements for various database queries
|
||||
|
||||
## Security
|
||||
- Remote lists, remote trails, and remote trail comments now respect the visibility settings and shares of the respective list or trail. (PR #980)
|
||||
|
||||
## Bug Fixes
|
||||
- Federated remote trails now sync tags and create missing local tags when needed. (PR #987)
|
||||
- Private profiles no longer prevent access to a user's own trails; inaccessible private profiles now return a proper `404`. (PR #986)
|
||||
- Comment access rules now correctly check the linked comment author via `author.user`. (PR #984)
|
||||
|
||||
# v0.19.0
|
||||
|
||||
## Breaking Changes
|
||||
- Bulk uploads no longer use `UPLOAD_USER` / `UPLOAD_PASSWORD` authentication. Uploads now require an API token; files must be placed in a subfolder of the upload directory named after the respective API token. For more information checkout the [documentation](https://wanderer.to/use/import-export/) (PR #886).
|
||||
- Bulk uploads now run via a file watcher rather than on a cron schedule. Files placed in the upload folder while the container is not running will not be processed automatically. (PR #886)
|
||||
- External service URLs have been moved from public frontend variables to server-side variables: `VALHALLA_URL`, `NOMINATIM_URL`, `OVERPASS_API_URL`. The old `PUBLIC_*` variables remain as a fallback but should be migrated. (PR #697)
|
||||
- The waypoint data model has been extended for federation: waypoints now have an `iri` and reference their author via an ActivityPub actor. This only affects clients that access PocketBase collections directly, not the standard UI. (PR #930)
|
||||
- ActivityPub actor counters have been renamed to `follower_count` and `following_count`. This only affects clients that access PocketBase collections directly. (PR #930)
|
||||
- OpenAPI documentation is now generated from annotations and served as JSON (the YAML endpoint has been removed). (PR #927)
|
||||
|
||||
## Security
|
||||
- HTML content in descriptions, comments, summit logs, waypoints, and profile bios is now sanitised on the server to reduce the risk of cross-site scripting (XSS). Some custom HTML may be stripped on save. (PR #930)
|
||||
- Anonymous user API endpoints have been removed. This only affects third-party applications that accessed user data without authentication. Regular users and the standard UI are unaffected. (PR #927)
|
||||
- Additional CSRF/SSRF protections and rate limiting have been implemented for ActivityPub and outbound network calls. (PR #930)
|
||||
|
||||
## Features
|
||||
- Hammerhead integration added, including synchronisation of planned and completed tours, and manual trail sending. (PR #628)
|
||||
- The federation has been significantly expanded and refactored to provide more robust remote content synchronisation and local caching for remote trails and lists. (PR #930)
|
||||
- Trails can now be explicitly marked as completed. (PR #920)
|
||||
- Geotagged waypoint photos are now automatically merged into existing or nearby waypoints within the configured merge radius of the category. (PR #457)
|
||||
- The trail overview now has narrow and wide view modes, as well as improved multi-select. (PR #666, #921)
|
||||
- Trails can be copied directly and their visibility can be changed more easily. (PR #571)
|
||||
- External geocoding, routing, and Overpass calls now run on the server. (PR #697)
|
||||
- New setting: optionally start drawing a new trail from the current location. (PR #592)
|
||||
- GPX exports now include additional metadata for waypoints. (PR #919)
|
||||
- FIT import now uses a more compatible parser. (PR #884)
|
||||
- Frontpage performance has been improved. (PR #929)
|
||||
- 3D terrain rendering has been improved. (PR #881)
|
||||
- Improved saving of public lists. (PR #554)
|
||||
- Multiple trails can now be merged into one trail with multiple summit logs. This feature is also available as an automatic option when importing trails via an integration (PR #627)
|
||||
- API tokens have been added so that external tools and automations can interact with Wanderer. (PR #848)
|
||||
|
||||
## Bug Fixes
|
||||
- Fixed broken WebFinger requests. (PR #966)
|
||||
- Theme detection fixed via corrected `color-scheme` query selector. (PR #957, thanks @mfortini)
|
||||
- Hillshading visibility on the map has been fixed. (PR #942)
|
||||
- Komoot sync now runs to completion. (PR #917, thanks @StefanSchloegl)
|
||||
- Komoot integration now handles invalid photos more robustly. (PR #941)
|
||||
- Mentions now work correctly when `username` and `preferred_username` differ. (PR #885)
|
||||
- Fixed avatar updates. (PR #870)
|
||||
- List descriptions in the selection modal are now fixed. (PR #869)
|
||||
- Fixed double GPX upload when creating a trail. (PR #969)
|
||||
- Fixed authentication issues after email change (PR #973)
|
||||
- Fixed help links. (PR #938)
|
||||
- Fixed a MapLibre layer manager issue that could prevent existing map layers from being tracked correctly after data updates. (PR #960, thanks @palhaland)
|
||||
- Fixed search endpoints returning invalid errors in some failure cases. (PR #961, thanks @palhaland)
|
||||
|
||||
## Translation
|
||||
- The Norwegian translations have been updated. (PR #931, thanks @palhaland)
|
||||
|
||||
## Maintenance
|
||||
- Meilisearch, PocketBase, Go, web/docs dependencies, CI actions, and Docker build setup updated.
|
||||
|
||||
|
||||
# v0.18.5
|
||||
## Security
|
||||
- Fixes CVE-2022-39299 via xmldom upgrade (PR #820)
|
||||
## Features
|
||||
- Persist trail list filter settings in local storage: filters are preserved on reload and when navigating back from a trail, and reset when - leaving the trail list (PR #814)
|
||||
## Bug Fixes
|
||||
- Skip elevation correction when Valhalla returns invalid (null) elevation data; original GPX values are preserved (PR #821)
|
||||
- Improved threshold handling for high-frequency GPX tracks to ensure correct ascent/descent calculation (PR #813)
|
||||
- Fixes trail upload for new users without default privacy settings (PR #785)
|
||||
- Fixes focus loss and duplicate “Cancel” button in trail filter search (Chrome) (PR #738)
|
||||
- Fixes async state issue in list search modal (bulk assignment works again) (PR #816)
|
||||
- Fixes locale errors and improved dynamic locale detection (PR #656)
|
||||
- Fixes PDF/print layout so descriptions render correctly and no longer overlap waypoints(Thanks to @RobertClarke64) (PR #797)
|
||||
- Corrected POCKETBASE_SMTP_SENDER_ADDRESS spelling (previously POCKETBASE_SMTP_SENDER_ADRESS, now deprecated but backwards compatible) (PR #769)
|
||||
- Fixes amenity naming in stored map state, including migration (PR #768)
|
||||
- Fixes race condition in map plugin that caused errors when printing a trail (PR #827)
|
||||
|
||||
## Translation
|
||||
- Added Czech language support (thanks @Sheepa) (PR #774)
|
||||
|
||||
## Dependencies
|
||||
- Updated most dependencies, including security and maintenance updates
|
||||
|
||||
# v0.18.4
|
||||
## Bug fixes
|
||||
- Tags can now properly be removed from trails
|
||||
- Creating more than 29 lists does no longer prevent lists from loading
|
||||
- Fixes a bug that caused the GPS data to be removed from summit logs after editing
|
||||
- Fixes an issue that caused an infinite loop when creating a list with federation being active
|
||||
- Fixes an issue where bicycle routing options were incorrectly applied to car routing, and vice versa
|
||||
- Fixes an issue where filtering by multiple categories did not work as expected
|
||||
- Fixes imported tracks being marked private despite public-by-default settings
|
||||
|
||||
## Features
|
||||
- Trails can now be added to multiple lists at once
|
||||
- ActivityPub: External user access now requires authentication (401)
|
||||
|
||||
## Translation
|
||||
- Adds Norwegian translation (thanks @palhaland)
|
||||
|
||||
# v0.18.3
|
||||
## Bug fixes
|
||||
- Fixes bug that prevented waypoints from being update or deleted in v0.18.2
|
||||
- Return properly formatted error message when accessing a protected API route
|
||||
- Fixes logo for OIDC 2 & 3 providers (thanks @wolffshots)
|
||||
- Fixes bug that caused dropdown actions from a feed item on the homepage to cause a 404 error
|
||||
|
||||
## Maintenance
|
||||
- Updates pocketbase to v0.30.0
|
||||
|
||||
# v0.18.2
|
||||
## Features
|
||||
- Adds `dedup` command to pocketbase. This command allows an admin to quickly identify duplicate trails and delete them. Use the `--dry-run` flag to only log duplicate trails without deleting them. To execute the command run `docker exec -it wanderer-db ./pocketbase dedup --dry-run`.
|
||||
- Adds option to only sync strava activities after a certain date
|
||||
- Singificant performance improvements for instances with larger userbases
|
||||
- Greatly improved initial indexing speed when starting wanderer
|
||||
## Bug fixes
|
||||
- Fixes permission issues for public trails
|
||||
- Fixes bug that caused trails to be duplicated multiple times (to clean up see the `dedup` command above)
|
||||
- Fixes link to "New Trail" from empty profiles
|
||||
- Fixes link when opening a trail from the map searchbar
|
||||
- Sorting by difficulty no longer sorts by difficulty alphabetically
|
||||
- Fixes strava integration stopping after only one page
|
||||
|
||||
# v0.18.1
|
||||
## Bug fixes
|
||||
- Fixes permission issues that prevented federation from working properly
|
||||
- Trail categories are properly displayed in lists
|
||||
- Fixes issue that prevented lists from saving
|
||||
|
||||
## Documentation
|
||||
- Improves docs for updating on bare-metal installations
|
||||
|
||||
## Translation
|
||||
- Adds Basque and Russian translations (thanks @aisaivia & @jeffscrum)
|
||||
|
||||
# v0.18.0
|
||||
|
||||
## Features
|
||||
- Adds feed on homepage for logged in users
|
||||
- Adds customizable "About" section to the homepage (read [here](https://wanderer.to/run/frontend-configuration/about) to learn more)
|
||||
- New Maps: OpenHikingMap, CyclOSM
|
||||
- New map overlays: hiking, biking, mountain biking & skiing routes
|
||||
- New POI overlays: e.g. grocery stores, campsites, water sources etc.
|
||||
- Complete overhaul of the route drawing/editing feature
|
||||
- Adds option to crop routes when editing
|
||||
- Adds undo/redo to route editing
|
||||
- Adds option to manually recalculate elevation data when editing a route
|
||||
- Adds automated elevation correction when uploading trails/summit logs directly
|
||||
- Directions to a trail head are now provided by OpenStreetMap
|
||||
- Non-pulbic trails can now be shared with guests via a public share link
|
||||
- Adds route previews on lower zoom levels in the world map
|
||||
> [!NOTE]
|
||||
To display the previews for each track on the world map, wanderer computes encoded polylines for each track on startup. Depending on the amount of trails in your instance, it can take several minutes until all trails are fully indexed and searchable.
|
||||
- 3D terrain is now also available in the world map
|
||||
- Trail category is now displayed in search results
|
||||
|
||||
## Bug fixes
|
||||
- Custom categories with spaces are now handled correctly
|
||||
- Avoid reuploading all trails when updating a list
|
||||
- Fixes build dependencies for building from source (thanks @slothful-vassal)
|
||||
- Add headers to nominatim requests to comply with usage policy
|
||||
- Reset pagination when updating filters
|
||||
|
||||
## Thanks
|
||||
|
||||
A big thanks goes to @cugu for doing a lot of GitHub house keeping and submitting various, helpful PRs while I was gone!
|
||||
Another big shoutout has to go to @vcoppe and [gpx.studio](https://github.com/gpxstudio/gpx.studio). Their code was a huge help implementing the various map overlays and GPX editing functionality that was added in this patch.
|
||||
|
||||
|
||||
# v0.17.2
|
||||
## Features
|
||||
- Trails in the map view can now be sorted
|
||||
- Adds ogp metadata tags for SEO
|
||||
- Public profiles are now accessible by anonymous users
|
||||
- A trail's direction can now be reversed in the editor
|
||||
- Adds localization for the calendar component (thanks @james-geiger)
|
||||
|
||||
## Bug fixes
|
||||
- Waypoint descriptions are now properly formatted
|
||||
- The summit log table on the statistics page shows data again
|
||||
- Fixes bug that caused lists to disappear when having more than 5 lists
|
||||
- Removing the hillshading URL is now possible
|
||||
- Clicking on a category on the homepage links to the correct trails page again
|
||||
- Fixes bug that caused trail to disappear when switching map style to OpenTopoMap
|
||||
- Trails are now correctly marked as "(Not) Completed" when adding (deleting) a summit log
|
||||
- Allow links to other sites when hosting a private instance
|
||||
|
||||
# v0.17.1
|
||||
|
||||
## Features
|
||||
|
||||
- Adds batch actions for trails. You can now select multiple trails from the list and add them to a list, for example. Big thanks to @slothful-vassal for the PR
|
||||
- If a trail has more than two tags they are now toggleable on trail cards for less visual clutter. Thank to @briannelson95 for the PR
|
||||
- The wanderer.to homepage now contains a dedicated "Servers" section where public instances are listed
|
||||
|
||||
## Bug fixes
|
||||
- Searching for a trail on the homepage does no longer result in 404
|
||||
- An actor's username and preferred username are no longer switched
|
||||
- Accesing your own private profile no longer throws an error
|
||||
- Reverse geocoding location lookup now also properly works when uploading a trail through the API
|
||||
- Both trail's and summit log's duration is now stored in seconds for consistency
|
||||
- All federated requests are now properly signed
|
||||
- Fixes GPX parser to deal with empty tags
|
||||
- Fixes bug that caused only 30 entries to be displayed in the statistics
|
||||
- The "Add to list" button is available again when creating a trail
|
||||
- Fixes bug that prevented saving lists with a large number of trails
|
||||
- Fixes "Copy link" button when sharing lists
|
||||
- Adds missing namespace to activitypub actor endpoint
|
||||
|
||||
# v0.17.0
|
||||
> [!CAUTION]
|
||||
This release contains breaking changes. They are marked with a ⚠️.
|
||||
**Please update to version v0.16.5 first before updating to v0.17.0.**
|
||||
|
||||
## Configuration
|
||||
Check the reopsitory's [`docker-compose.yml`](https://github.com/Flomp/wanderer/blob/main/docker-compose.yml) for a valid configuration.
|
||||
Check the reopsitory's [`docker-compose.yml`](https://github.com/open-wanderer/wanderer/blob/main/docker-compose.yml) for a valid configuration.
|
||||
|
||||
- ⚠️ The PocketBase environment variable `POCKETBASE_ENCRYPTION_KEY` is now required. It requires a valid 32 character AES key as its value. To generate a key, run `openssl rand -hex 16`.
|
||||
- ⚠️ The PocketBase environment variable `ORIGIN`is now required. It must be set to the public IP or hostname (including the port) of your wanderer frontend and must equal the value set for the frontend's `ORIGIN` environment variable.
|
||||
@@ -118,7 +335,7 @@ This release contains breaking changes. The necessary migrations will happen aut
|
||||
- Waypoints can now be created by clicking on the map when creating a new trail
|
||||
- Adds support for videos
|
||||
|
||||
## Bugfixes
|
||||
## Bug fixes
|
||||
- Fixes map trail bounding box to include public and shared trails
|
||||
- Fixes bug that caused orphan waypoints and summit logs
|
||||
- The default language is now set correctly after registering
|
||||
@@ -253,7 +470,7 @@ This release contains breaking changes. Most migrations will happen automaticall
|
||||
|
||||
## Maintenance
|
||||
- Updates to meilisearch version 0.11.3.
|
||||
- ⚠️ meilisearch indices are not compatible across minor versions. This means you will need to rename or delete your [`data.ms`](https://github.com/Flomp/wanderer/blob/8635de78b9f1510e2316b08e605b175a2615f4db/docker-compose.yml#L19) folder on your host system to force meilisearch to rebuild the index on the next start (note that this can take a little while).
|
||||
- ⚠️ meilisearch indices are not compatible across minor versions. This means you will need to rename or delete your [`data.ms`](https://github.com/open-wanderer/wanderer/blob/8635de78b9f1510e2316b08e605b175a2615f4db/docker-compose.yml#L19) folder on your host system to force meilisearch to rebuild the index on the next start (note that this can take a little while).
|
||||
|
||||
## Features
|
||||
- Adds password reset email function for users (see [docs](https://wanderer.to/guides/authentication/#forgot-your-password) for more info)
|
||||
@@ -327,7 +544,7 @@ As the number of contributors to this project continues to grow (which I’m ver
|
||||
- The current page is now remembered when navigating back to the trail overview
|
||||
|
||||
## Bug fixes
|
||||
- Fixed map height when viewing a trail in detail view
|
||||
- Fixes map height when viewing a trail in detail view
|
||||
|
||||
# v0.8.1
|
||||
## Features
|
||||
@@ -404,7 +621,7 @@ As the number of contributors to this project continues to grow (which I’m ver
|
||||
- Adds missing translations
|
||||
|
||||
## Bug fixes
|
||||
- Fixed a bug that prevented comments from showing up
|
||||
- Fixes a bug that prevented comments from showing up
|
||||
- GPX files without a name in the metadata section will now receive a generic name when uploaded through the API
|
||||
|
||||
# v0.5.1
|
||||
@@ -412,7 +629,7 @@ As the number of contributors to this project continues to grow (which I’m ver
|
||||
- You can now export trails as GPX or GEOJson files. Optionally you can include photos and summit book entries of the trail. This replaces the "Download GPX" function in previous versions.
|
||||
|
||||
## Bug fixes
|
||||
- Fixed a bug that would prevent users from creating multiple summit log entries without reloading the page
|
||||
- Fixes a bug that would prevent users from creating multiple summit log entries without reloading the page
|
||||
- wanderer now takes the `<rte>` tag into account when displaying a trail on the map
|
||||
- A trail's date attribute is now the current date by default
|
||||
|
||||
@@ -424,7 +641,7 @@ As the number of contributors to this project continues to grow (which I’m ver
|
||||
## Features
|
||||
- Trails can now be filtered by date
|
||||
- Elevation, slope and speed graphs are now also visible when creating a new trail
|
||||
- When creating a new trail you now have the option to create a new route from scratch without uploading a GPX file. Press the "Draw a route" button and plan your new route directly in wanderer. We use [valhalla](https://github.com/valhalla/valhalla) and their associated free [hosted service](https://gis-ops.com/global-open-valhalla-server-online/) to calculate the routes. To activate the feature make sure to set the PUBLIC_VALHALLA_URL environment variable on you wanderer-web service. See the current [docker-compose.yml](https://github.com/Flomp/wanderer/blob/main/docker-compose.yml) for a working configuration.
|
||||
- When creating a new trail you now have the option to create a new route from scratch without uploading a GPX file. Press the "Draw a route" button and plan your new route directly in wanderer. We use [valhalla](https://github.com/valhalla/valhalla) and their associated free [hosted service](https://gis-ops.com/global-open-valhalla-server-online/) to calculate the routes. To activate the feature make sure to set the PUBLIC_VALHALLA_URL environment variable on you wanderer-web service. See the current [docker-compose.yml](https://github.com/open-wanderer/wanderer/blob/main/docker-compose.yml) for a working configuration.
|
||||
|
||||
## Bug fixes
|
||||
- Uploaded trails will now have a date if it can be parsed from the file
|
||||
@@ -438,13 +655,13 @@ As the number of contributors to this project continues to grow (which I’m ver
|
||||
- Adds a setting to focus the map on all trails instead of a specific location
|
||||
|
||||
## Bug fixes
|
||||
- fixed a bug that would show a wrong date for summit logs for certain time zones (now really)
|
||||
- fixed a bug that prevented waypoints from showing up in public trails
|
||||
- Fixes a bug that would show a wrong date for summit logs for certain time zones (now really)
|
||||
- Fixes a bug that prevented waypoints from showing up in public trails
|
||||
|
||||
# v0.3.2
|
||||
## Bug fixes
|
||||
|
||||
- Fixed a bug that caused a 500 Internal Error to appear when viewing trails without an account
|
||||
- Fixes a bug that caused a 500 Internal Error to appear when viewing trails without an account
|
||||
|
||||
## Translations
|
||||
|
||||
@@ -459,7 +676,7 @@ As the number of contributors to this project continues to grow (which I’m ver
|
||||
|
||||
## Bug fixes
|
||||
|
||||
- Fixed a bug that prevented import trails from appearing in map view
|
||||
- Fixes a bug that prevented import trails from appearing in map view
|
||||
|
||||
## Translations
|
||||
|
||||
@@ -473,13 +690,13 @@ As the number of contributors to this project continues to grow (which I’m ver
|
||||
## Features
|
||||
|
||||
- Trails can now be added to a list while editing or creating a trail. The trail must be saved at least once to add it to a list.
|
||||
- wanderer now has an auto-upload folder. GPX files in this folder will be autmatically uploaded and converted to a trail. Read the [docs](https://github.com/Flomp/wanderer/wiki/API#auto-upload-folder) for more information.
|
||||
- wanderer now has an auto-upload folder. GPX files in this folder will be autmatically uploaded and converted to a trail. Read the [docs](https://github.com/open-wanderer/wanderer/wiki/API#auto-upload-folder) for more information.
|
||||
- addded support for TCX and KML files. Note that this feature is still experimental. Please report any issues you encounter.
|
||||
- added OAuth support. Read [here](https://github.com/Flomp/wanderer/wiki/OAuth) how to enable providers.
|
||||
- added OAuth support. Read [here](https://github.com/open-wanderer/wanderer/wiki/OAuth) how to enable providers.
|
||||
|
||||
## Bug fixes
|
||||
|
||||
- fixed a bug that would show a wrong date for summit logs for certain time zones
|
||||
- Fixes a bug that would show a wrong date for summit logs for certain time zones
|
||||
- added client side validation for usernames
|
||||
|
||||
## Translations
|
||||
@@ -491,7 +708,7 @@ As the number of contributors to this project continues to grow (which I’m ver
|
||||
## Bug fixes
|
||||
|
||||
- summit book dates now show in the correct format for the current locale
|
||||
- fixed a bug that would overwrite trail names and descriptions when editing a trail
|
||||
- Fixes a bug that would overwrite trail names and descriptions when editing a trail
|
||||
|
||||
## Translations
|
||||
|
||||
@@ -508,7 +725,7 @@ As the number of contributors to this project continues to grow (which I’m ver
|
||||
- waypoint markers can now be moved with drag & drop
|
||||
- lists can now be displayed as a map showing all trails contained in the list
|
||||
- you can now prevent users from signing up by setting the `DISABLE_SIGNUP` environment variable to `true`
|
||||
- you can now upload GPX files via the API to create trails. Check the [documentation](https://github.com/Flomp/wanderer/wiki/API#upload-trails) for more info.
|
||||
- you can now upload GPX files via the API to create trails. Check the [documentation](https://github.com/open-wanderer/wanderer/wiki/API#upload-trails) for more info.
|
||||
- the city index now includes states
|
||||
|
||||
> Note: for city states to show up in your search you have to delete your data.ms folder if you already have a previous installation of wanderer. The indices will then be rebuilt on startup.
|
||||
@@ -520,10 +737,10 @@ As the number of contributors to this project continues to grow (which I’m ver
|
||||
# v0.1.1
|
||||
## Bug fixes
|
||||
|
||||
- fixed a bug that would prevent trails longer than 20km from being displayed
|
||||
- Fixes a bug that would prevent trails longer than 20km from being displayed
|
||||
- added BODY_SIZE_LIMIT env variable to docker compose to allow for bigger file uploads
|
||||
- fixed a bug that caused only 5 trails to be shown at a time
|
||||
- fixed a bug that would cause waypoints not to be deleted from the backend
|
||||
- Fixes a bug that caused only 5 trails to be shown at a time
|
||||
- Fixes a bug that would cause waypoints not to be deleted from the backend
|
||||
- updated the default docker-compose.yml to include a secure MEILI_MASTER_KEY
|
||||
- the default location field now sets the value correctly after clicking on a search result
|
||||
|
||||
@@ -532,4 +749,4 @@ As the number of contributors to this project continues to grow (which I’m ver
|
||||
- updated the docs to include BODY_SIZE_LIMIT
|
||||
|
||||
# v0.1.0
|
||||
- Initial release
|
||||
- Initial release
|
||||
|
||||
43
Makefile
Normal file
43
Makefile
Normal file
@@ -0,0 +1,43 @@
|
||||
## db
|
||||
|
||||
.PHONY: db-fmt
|
||||
db-fmt:
|
||||
cd db && go fmt ./...
|
||||
|
||||
.PHONY: db-vet
|
||||
db-vet:
|
||||
cd db && go vet ./...
|
||||
|
||||
.PHONY: db-test
|
||||
db-test:
|
||||
cd db && go test ./...
|
||||
|
||||
.PHONY: db-build
|
||||
db-build:
|
||||
cd db && CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o pocketbase_amd64
|
||||
|
||||
.PHONY: db-build-docker
|
||||
db-build-docker: db-build
|
||||
docker buildx build db/ --no-cache -t flomp/wanderer-db:latest
|
||||
|
||||
## Web
|
||||
|
||||
.PHONY: web-install
|
||||
web-install:
|
||||
cd web && npm install
|
||||
|
||||
.PHONY: web-playwright-install
|
||||
web-playwright-install:
|
||||
cd web && npx playwright install --with-deps chromium
|
||||
|
||||
.PHONY: web-check
|
||||
web-check:
|
||||
cd web && npm run check
|
||||
|
||||
.PHONY: web-test
|
||||
web-test:
|
||||
cd web && npm run test
|
||||
|
||||
.PHONY: web-build-docker
|
||||
web-build-docker:
|
||||
docker buildx build web/ --no-cache -t flomp/wanderer-web:latest
|
||||
@@ -4,8 +4,8 @@
|
||||
|
||||
<h4>The trail catalogue that makes your GPS data searchable</h4>
|
||||
|
||||
[](https://github.com/Flomp/wanderer/)
|
||||
[](https://github.com/Flomp/wanderer/)
|
||||
[](https://github.com/open-wanderer/wanderer/)
|
||||
[](https://github.com/open-wanderer/wanderer/)
|
||||
[](https://www.buymeacoffee.com/wanderertrails)
|
||||
[](https://discord.gg/USSEBY98CP)
|
||||
|
||||
@@ -32,7 +32,7 @@ The recommended and quickest way to install wanderer is using docker compose:
|
||||
|
||||
``` bash
|
||||
# download the docker compose file
|
||||
wget https://raw.githubusercontent.com/Flomp/wanderer/main/docker-compose.yml
|
||||
wget https://raw.githubusercontent.com/open-wanderer/wanderer/main/docker-compose.yml
|
||||
|
||||
# build and launch via docker compose
|
||||
docker compose up -d
|
||||
@@ -44,7 +44,7 @@ The first startup can take up to 90 seconds after which you can access the front
|
||||
|
||||
> ⚠️ if you are using wanderer in a production environment make sure to change the MEILI_MASTER_KEY variable.
|
||||
|
||||
You can also run wanderer on bare-metal. Check out the [documentation](https://wanderer.to/run/installation/#installation-from-source) for a detailed how-to guide.
|
||||
You can also run wanderer on bare-metal. Check out the [documentation](https://wanderer.to/run/installation/from-source) for a detailed how-to guide.
|
||||
|
||||
## Support wanderer
|
||||
|
||||
|
||||
12
db/.dockerignore
Normal file
12
db/.dockerignore
Normal file
@@ -0,0 +1,12 @@
|
||||
*
|
||||
!commands
|
||||
!federation
|
||||
!hooks
|
||||
!go.*
|
||||
!integrations
|
||||
!main.go
|
||||
!migrations
|
||||
!routes
|
||||
!templates
|
||||
!services
|
||||
!util
|
||||
@@ -1,18 +1,56 @@
|
||||
FROM alpine:3.16
|
||||
FROM curlimages/curl:8.18.0 AS download-env
|
||||
|
||||
# renovate: datasource=github-releases depName=stunnel/static-curl packageName=stunnel/static-curl
|
||||
ENV CURL_VERSION=8.18.0
|
||||
|
||||
RUN set -eux ; \
|
||||
ARCHITECTURE="$(uname -m)" ; \
|
||||
case $ARCHITECTURE in \
|
||||
x86_64) ARCHITECTURE="x86_64" ;; \
|
||||
aarch64 | armv8* | arm64) ARCHITECTURE="aarch64" ;; \
|
||||
*) \
|
||||
echo "(!) Architecture $ARCHITECTURE unsupported" ; \
|
||||
exit 1 \
|
||||
;; \
|
||||
esac ; \
|
||||
curl \
|
||||
--connect-timeout 10 \
|
||||
--fail \
|
||||
--location \
|
||||
--max-time 300 \
|
||||
--output /tmp/curl.tar.xz \
|
||||
--proto '=https' \
|
||||
--show-error \
|
||||
--silent \
|
||||
--tlsv1.2 \
|
||||
"https://github.com/stunnel/static-curl/releases/download/${CURL_VERSION}/curl-linux-${ARCHITECTURE}-glibc-${CURL_VERSION}.tar.xz" \
|
||||
; \
|
||||
tar -xJf /tmp/curl.tar.xz -C /tmp ; \
|
||||
chmod +x /tmp/curl ;
|
||||
|
||||
FROM golang:1.25.0@sha256:5502b0e56fca23feba76dbc5387ba59c593c02ccc2f0f7355871ea9a0852cebe AS build
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY . .
|
||||
|
||||
RUN go mod download
|
||||
RUN CGO_ENABLED=0 go build -ldflags="-s -w" -o /app/pocketbase
|
||||
|
||||
FROM scratch
|
||||
|
||||
WORKDIR /
|
||||
|
||||
COPY --from=build /app/pocketbase /pocketbase
|
||||
COPY --from=build /etc/ssl/certs /etc/ssl/certs
|
||||
COPY --from=download-env /tmp/curl /curl
|
||||
COPY migrations ./migrations
|
||||
COPY templates ./templates
|
||||
|
||||
ARG TARGETARCH
|
||||
RUN echo ${TARGETARCH}
|
||||
COPY ./pocketbase_${TARGETARCH} /pocketbase
|
||||
RUN chmod +x /pocketbase
|
||||
|
||||
ENV MEILI_URL=http://localhost:7700
|
||||
ENV MEILI_MASTER_KEY=
|
||||
ENV MEILI_URL=http://localhost:7700 \
|
||||
MEILI_MASTER_KEY= \
|
||||
POCKETBASE_ENCRYPTION_KEY=
|
||||
|
||||
EXPOSE 8090
|
||||
|
||||
ENTRYPOINT ["/pocketbase", "serve", "--http=0.0.0.0:8090", "--dir=/pb_data"]
|
||||
ENTRYPOINT ["/pocketbase", "serve", "--http=0.0.0.0:8090", "--dir=/pb_data"]
|
||||
|
||||
121
db/commands/dedup.go
Normal file
121
db/commands/dedup.go
Normal file
@@ -0,0 +1,121 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"crypto/sha1"
|
||||
"fmt"
|
||||
"log"
|
||||
"sort"
|
||||
|
||||
"github.com/pocketbase/pocketbase"
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func Dedup(app *pocketbase.PocketBase) *cobra.Command {
|
||||
var dryRun bool
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "dedup",
|
||||
Short: "Deduplicate trails by all matching fields",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
records, err := app.FindAllRecords("trails")
|
||||
if err != nil {
|
||||
log.Fatalf("failed to fetch trails: %v", err)
|
||||
}
|
||||
|
||||
// group by composite key
|
||||
trailsByKey := make(map[string][]*core.Record)
|
||||
for _, r := range records {
|
||||
key := makeKey(r)
|
||||
trailsByKey[key] = append(trailsByKey[key], r)
|
||||
}
|
||||
|
||||
var duplicates []*core.Record
|
||||
for _, recs := range trailsByKey {
|
||||
if len(recs) <= 1 {
|
||||
continue
|
||||
}
|
||||
|
||||
// sort by created date ascending
|
||||
sort.Slice(recs, func(i, j int) bool {
|
||||
return recs[i].GetDateTime("created").Time().Before(recs[j].GetDateTime("created").Time())
|
||||
})
|
||||
|
||||
original := recs[0]
|
||||
dupes := recs[1:]
|
||||
|
||||
// print header row for original
|
||||
// print original as header
|
||||
fmt.Printf("\nOriginal: id=%s, name=%s, distance=%.2f, elevation_gain=%.2f, elevation_loss=%.2f, lat=%.5f, lon=%.5f, duration=%.2f, location=%s, category=%s, author=%s, created=%s\n",
|
||||
original.Id,
|
||||
original.GetString("name"),
|
||||
original.GetFloat("distance"),
|
||||
original.GetFloat("elevation_gain"),
|
||||
original.GetFloat("elevation_loss"),
|
||||
original.GetFloat("lat"),
|
||||
original.GetFloat("lon"),
|
||||
original.GetFloat("duration"),
|
||||
original.GetString("location"),
|
||||
original.GetString("category"),
|
||||
original.GetString("author"),
|
||||
original.GetDateTime("created"),
|
||||
)
|
||||
|
||||
// print duplicates indented
|
||||
for _, d := range dupes {
|
||||
fmt.Printf(" Duplicate: id=%s, name=%s, distance=%.2f, elevation_gain=%.2f, elevation_loss=%.2f, lat=%.5f, lon=%.5f, duration=%.2f, location=%s, category=%s, author=%s, created=%s\n",
|
||||
d.Id,
|
||||
d.GetString("name"),
|
||||
d.GetFloat("distance"),
|
||||
d.GetFloat("elevation_gain"),
|
||||
d.GetFloat("elevation_loss"),
|
||||
d.GetFloat("lat"),
|
||||
d.GetFloat("lon"),
|
||||
d.GetFloat("duration"),
|
||||
d.GetString("location"),
|
||||
d.GetString("category"),
|
||||
d.GetString("author"),
|
||||
d.GetDateTime("created"),
|
||||
)
|
||||
duplicates = append(duplicates, d)
|
||||
}
|
||||
}
|
||||
|
||||
if dryRun {
|
||||
fmt.Printf("\n[Dry Run] Found %d duplicates (no deletions performed)\n", len(duplicates))
|
||||
return
|
||||
}
|
||||
|
||||
// delete duplicates
|
||||
for _, d := range duplicates {
|
||||
if err := app.Delete(d); err != nil {
|
||||
fmt.Printf("Failed to delete duplicate %s: %v\n", d.Id, err)
|
||||
} else {
|
||||
fmt.Printf("Deleted duplicate %s\n", d.Id)
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().BoolVar(&dryRun, "dry-run", false, "Show duplicates without deleting them")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
// makeKey creates a composite key string for duplicate detection
|
||||
func makeKey(r *core.Record) string {
|
||||
data := fmt.Sprintf("%s|%f|%f|%f|%f|%f|%f|%s|%s|%s",
|
||||
r.GetString("name"),
|
||||
r.GetFloat("distance"),
|
||||
r.GetFloat("elevation_gain"),
|
||||
r.GetFloat("elevation_loss"),
|
||||
r.GetFloat("lat"),
|
||||
r.GetFloat("lon"),
|
||||
r.GetFloat("duration"),
|
||||
r.GetString("location"),
|
||||
r.GetString("category"),
|
||||
r.GetString("author"),
|
||||
)
|
||||
h := sha1.Sum([]byte(data))
|
||||
return fmt.Sprintf("%x", h)
|
||||
}
|
||||
@@ -4,12 +4,9 @@ import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/x509"
|
||||
"database/sql"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"slices"
|
||||
"strings"
|
||||
@@ -27,196 +24,103 @@ import (
|
||||
)
|
||||
|
||||
func PostActivity(app core.App, actor *core.Record, activity *pub.Activity, recipients []string) error {
|
||||
encryptionKey := os.Getenv("POCKETBASE_ENCRYPTION_KEY")
|
||||
if len(encryptionKey) == 0 {
|
||||
return fmt.Errorf("POCKETBASE_ENCRYPTION_KEY not set")
|
||||
}
|
||||
origin := os.Getenv("ORIGIN")
|
||||
if origin == "" {
|
||||
return fmt.Errorf("ORIGIN not set")
|
||||
}
|
||||
|
||||
algs := []httpsig.Algorithm{httpsig.RSA_SHA256}
|
||||
postHeaders := []string{"(request-target)", "Date", "Digest", "Content-Type", "Host"}
|
||||
expiresIn := 60
|
||||
|
||||
body, err := jsonld.WithContext(
|
||||
jsonld.IRI(pub.ActivityBaseURI),
|
||||
jsonld.IRI(pub.SecurityContextURI),
|
||||
).Marshal(activity)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
decryptedPrivateKey, err := security.Decrypt(actor.GetString("private_key"), encryptionKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
privateKey, err := x509.ParsePKCS1PrivateKey(decryptedPrivateKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
pubID := actor.GetString("iri") + "#main-key"
|
||||
|
||||
client := &http.Client{}
|
||||
var wg sync.WaitGroup
|
||||
|
||||
sem := semaphore.NewWeighted(5) // Limit to 5 concurrent sends
|
||||
|
||||
slices.Sort(recipients)
|
||||
uniqueRecipients := slices.Compact(recipients)
|
||||
|
||||
for _, v := range uniqueRecipients {
|
||||
|
||||
wg.Add(1)
|
||||
go func(inbox string) {
|
||||
defer wg.Done()
|
||||
|
||||
signer, _, err := httpsig.NewSigner(algs, httpsig.DigestSha256, postHeaders, httpsig.Signature, int64(expiresIn))
|
||||
if err != nil {
|
||||
return
|
||||
go func() {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
app.Logger().Error(fmt.Sprintf("Recovered from panic in PostActivity: %v", r))
|
||||
}
|
||||
}()
|
||||
|
||||
if err := sem.Acquire(context.Background(), 1); err != nil {
|
||||
app.Logger().Error(fmt.Sprintf("Semaphore acquire failed: %s", err))
|
||||
return
|
||||
}
|
||||
defer sem.Release(1)
|
||||
encryptionKey := os.Getenv("POCKETBASE_ENCRYPTION_KEY")
|
||||
if len(encryptionKey) == 0 {
|
||||
app.Logger().Error("POCKETBASE_ENCRYPTION_KEY not set")
|
||||
return
|
||||
}
|
||||
origin := os.Getenv("ORIGIN")
|
||||
if origin == "" {
|
||||
app.Logger().Error("ORIGIN not set")
|
||||
return
|
||||
}
|
||||
|
||||
buf := bytes.NewBuffer(body)
|
||||
req, err := http.NewRequest(http.MethodPost, inbox, buf)
|
||||
if err != nil {
|
||||
app.Logger().Error(fmt.Sprintf("Request creation failed: %s", err))
|
||||
return
|
||||
}
|
||||
req.Header.Add("Content-Type", "application/activity+json")
|
||||
req.Header.Add("Date", strings.ReplaceAll(time.Now().UTC().Format(time.RFC1123), "UTC", "GMT"))
|
||||
req.Header.Add("Host", req.Host)
|
||||
algs := []httpsig.Algorithm{httpsig.RSA_SHA256}
|
||||
postHeaders := []string{"(request-target)", "Date", "Digest", "Content-Type", "Host"}
|
||||
expiresIn := 60
|
||||
|
||||
if err := signer.SignRequest(privateKey, pubID, req, body); err != nil {
|
||||
app.Logger().Error(fmt.Sprintf("Signing request failed: %s", err))
|
||||
return
|
||||
}
|
||||
body, err := jsonld.WithContext(
|
||||
jsonld.IRI(pub.ActivityBaseURI),
|
||||
jsonld.IRI(pub.SecurityContextURI),
|
||||
).Marshal(activity)
|
||||
if err != nil {
|
||||
app.Logger().Error(fmt.Sprintf("Failed to marshal activity: %s", err))
|
||||
return
|
||||
}
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
app.Logger().Error(fmt.Sprintf("Error sending request to inbox %s: %s", inbox, err))
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
decryptedPrivateKey, err := security.Decrypt(actor.GetString("private_key"), encryptionKey)
|
||||
if err != nil {
|
||||
app.Logger().Error(fmt.Sprintf("Failed to decrypt key: %s", err))
|
||||
return
|
||||
}
|
||||
privateKey, err := x509.ParsePKCS1PrivateKey(decryptedPrivateKey)
|
||||
if err != nil {
|
||||
app.Logger().Error(fmt.Sprintf("Failed to parse private key: %s", err))
|
||||
return
|
||||
}
|
||||
pubID := actor.GetString("iri") + "#main-key"
|
||||
|
||||
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusAccepted {
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
app.Logger().Error(fmt.Sprintf("Inbox %s responded with %d: %s", inbox, resp.StatusCode, body))
|
||||
} else {
|
||||
app.Logger().Info(fmt.Sprintf("Sent %s to %s", activity.Type, inbox), "activity", activity)
|
||||
}
|
||||
client := &http.Client{}
|
||||
sem := semaphore.NewWeighted(5)
|
||||
|
||||
}(v)
|
||||
}
|
||||
slices.Sort(recipients)
|
||||
uniqueRecipients := slices.Compact(recipients)
|
||||
|
||||
wg.Wait()
|
||||
var wg sync.WaitGroup
|
||||
for _, v := range uniqueRecipients {
|
||||
wg.Add(1)
|
||||
go func(inbox string) {
|
||||
defer wg.Done()
|
||||
|
||||
signer, _, err := httpsig.NewSigner(algs, httpsig.DigestSha256, postHeaders, httpsig.Signature, int64(expiresIn))
|
||||
if err != nil {
|
||||
app.Logger().Error(fmt.Sprintf("Signer creation failed: %s", err))
|
||||
return
|
||||
}
|
||||
|
||||
if err := sem.Acquire(context.Background(), 1); err != nil {
|
||||
app.Logger().Error(fmt.Sprintf("Semaphore acquire failed: %s", err))
|
||||
return
|
||||
}
|
||||
defer sem.Release(1)
|
||||
|
||||
req, err := http.NewRequest(http.MethodPost, inbox, bytes.NewBuffer(body))
|
||||
if err != nil {
|
||||
app.Logger().Error(fmt.Sprintf("Request creation failed: %s", err))
|
||||
return
|
||||
}
|
||||
req.Header.Add("Content-Type", "application/activity+json")
|
||||
req.Header.Add("Date", strings.ReplaceAll(time.Now().UTC().Format(time.RFC1123), "UTC", "GMT"))
|
||||
req.Header.Add("Host", req.Host)
|
||||
|
||||
if err := signer.SignRequest(privateKey, pubID, req, body); err != nil {
|
||||
app.Logger().Error(fmt.Sprintf("Signing request failed: %s", err))
|
||||
return
|
||||
}
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
app.Logger().Error(fmt.Sprintf("Error sending to inbox %s: %s", inbox, err))
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusAccepted {
|
||||
respBody, _ := io.ReadAll(resp.Body)
|
||||
app.Logger().Error(fmt.Sprintf("Inbox %s responded with %d: %s", inbox, resp.StatusCode, respBody))
|
||||
} else {
|
||||
app.Logger().Info(fmt.Sprintf("Sent %s to %s", activity.Type, inbox), "activity", activity)
|
||||
}
|
||||
}(v)
|
||||
}
|
||||
wg.Wait()
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
|
||||
func ProcessActivity(e *core.RequestEvent) error {
|
||||
origin := os.Getenv("ORIGIN")
|
||||
if origin == "" {
|
||||
return fmt.Errorf("ORIGIN not set")
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(e.Request.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var activity pub.Activity
|
||||
activity.UnmarshalJSON(body)
|
||||
|
||||
inbox := fmt.Sprintf("%s%s", origin, e.Request.Header.Get("X-Forwarded-Path"))
|
||||
|
||||
userActor, err := e.App.FindFirstRecordByData("activitypub_actors", "inbox", inbox)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
actor, err := e.App.FindFirstRecordByData("activitypub_actors", "iri", activity.Actor.GetID().String())
|
||||
if err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
actor, err = GetActorByIRI(e.App, userActor, activity.Actor.GetID().String(), false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
return err
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
verified, err := verifySignature(e.App, e.Request, actor.GetString("public_key"))
|
||||
if err != nil || !verified {
|
||||
e.App.Logger().Error(err.Error())
|
||||
return e.UnauthorizedError("Invalid http signature", err)
|
||||
}
|
||||
|
||||
switch activity.Type {
|
||||
case pub.FollowType:
|
||||
ProcessFollowActivity(e.App, actor, activity)
|
||||
case pub.AcceptType:
|
||||
ProcessAcceptActivity(e.App, actor, activity)
|
||||
case pub.UndoType:
|
||||
ProcessUndoActivity(e.App, actor, activity)
|
||||
case pub.UpdateType:
|
||||
fallthrough
|
||||
case pub.CreateType:
|
||||
ProcessCreateOrUpdateActivity(e.App, actor, activity)
|
||||
case pub.DeleteType:
|
||||
ProcessDeleteActivity(e.App, actor, activity)
|
||||
case pub.AnnounceType:
|
||||
ProcessAnnounceActivity(e.App, actor, activity)
|
||||
case pub.LikeType:
|
||||
ProcessLikeActivity(e.App, actor, activity)
|
||||
}
|
||||
return e.JSON(http.StatusOK, nil)
|
||||
}
|
||||
|
||||
func verifySignature(app core.App, req *http.Request, publicKeyPem string) (bool, error) {
|
||||
origin := os.Getenv("ORIGIN")
|
||||
if origin == "" {
|
||||
return false, fmt.Errorf("ORIGIN not set")
|
||||
}
|
||||
block, _ := pem.Decode([]byte(publicKeyPem))
|
||||
if block == nil || block.Type != "PUBLIC KEY" {
|
||||
return false, fmt.Errorf("could not decode publicKeyPem to PUBLIC KEY pem block type")
|
||||
}
|
||||
|
||||
req.URL = &url.URL{
|
||||
Path: req.Header.Get("X-Forwarded-Path"),
|
||||
}
|
||||
|
||||
url, err := url.Parse(origin)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
req.Header.Set("Host", url.Host)
|
||||
req.Host = url.Host
|
||||
|
||||
app.Logger().Info(req.Header.Get("signature"))
|
||||
|
||||
publicKey, err := x509.ParsePKIXPublicKey(block.Bytes)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
v, err := httpsig.NewVerifier(req)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
err = v.Verify(publicKey, httpsig.RSA_SHA256)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
package federation
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/x509"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"pocketbase/util"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -19,6 +23,9 @@ import (
|
||||
"github.com/pocketbase/pocketbase/tools/security"
|
||||
)
|
||||
|
||||
var ErrProfilePrivate = errors.New("profile is private")
|
||||
var ErrInvalidActorResponse = errors.New("invalid or incomplete actor response")
|
||||
|
||||
type WebfingerResponse struct {
|
||||
Subject string `json:"subject"`
|
||||
Links []struct {
|
||||
@@ -27,24 +34,36 @@ type WebfingerResponse struct {
|
||||
} `json:"links"`
|
||||
}
|
||||
|
||||
func SplitHandle(handle string) (string, string) {
|
||||
|
||||
cleaned := strings.TrimPrefix(handle, "@")
|
||||
cleaned = strings.TrimSpace(cleaned)
|
||||
|
||||
if !strings.Contains(cleaned, "@") {
|
||||
return cleaned, ""
|
||||
func validateActorResponse(actor *pub.Actor) error {
|
||||
if actor == nil {
|
||||
return ErrInvalidActorResponse
|
||||
}
|
||||
|
||||
parts := strings.SplitN(cleaned, "@", 2)
|
||||
user := parts[0]
|
||||
domain := parts[1]
|
||||
if actor.GetID().String() == "" {
|
||||
return fmt.Errorf("%w: missing ID", ErrInvalidActorResponse)
|
||||
}
|
||||
|
||||
return user, domain
|
||||
if actor.PreferredUsername.String() == "" && actor.Name.String() == "" {
|
||||
return fmt.Errorf("%w: missing username or name", ErrInvalidActorResponse)
|
||||
}
|
||||
|
||||
if util.ItemID(actor.Inbox) == "" {
|
||||
return fmt.Errorf("%w: missing inbox", ErrInvalidActorResponse)
|
||||
}
|
||||
|
||||
if util.ItemID(actor.Outbox) == "" {
|
||||
return fmt.Errorf("%w: missing outbox", ErrInvalidActorResponse)
|
||||
}
|
||||
|
||||
if actor.PublicKey.PublicKeyPem == "" {
|
||||
return fmt.Errorf("%w: missing public key", ErrInvalidActorResponse)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func GetActorByHandle(app core.App, actor *core.Record, handle string, includeFollows bool) (*core.Record, error) {
|
||||
username, domain := SplitHandle(handle)
|
||||
func GetActorByHandle(app core.App, ctx context.Context, handle string, includeFollows bool) (*core.Record, error) {
|
||||
username, domain := util.SplitHandle(handle)
|
||||
|
||||
filter := "preferred_username={:username}&&"
|
||||
if domain != "" {
|
||||
@@ -63,7 +82,7 @@ func GetActorByHandle(app core.App, actor *core.Record, handle string, includeFo
|
||||
|
||||
dbActor = core.NewRecord(collection)
|
||||
dbActor.Set("isLocal", false)
|
||||
iri, err := iriFromHandle(domain, username)
|
||||
iri, err := iriFromHandle(ctx, domain, username)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -73,10 +92,10 @@ func GetActorByHandle(app core.App, actor *core.Record, handle string, includeFo
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return assembleActor(actor, dbActor, app, includeFollows)
|
||||
return assembleActor(app, ctx, dbActor, includeFollows || dbActor.Id == "")
|
||||
}
|
||||
|
||||
func GetActorByIRI(app core.App, actor *core.Record, iri string, includeFollows bool) (*core.Record, error) {
|
||||
func GetActorByIRI(app core.App, ctx context.Context, iri string, includeFollows bool) (*core.Record, error) {
|
||||
var dbActor *core.Record
|
||||
dbActor, err := app.FindFirstRecordByFilter("activitypub_actors", "iri={:iri}", dbx.Params{"iri": iri})
|
||||
if err != nil && err == sql.ErrNoRows {
|
||||
@@ -93,33 +112,55 @@ func GetActorByIRI(app core.App, actor *core.Record, iri string, includeFollows
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return assembleActor(actor, dbActor, app, includeFollows)
|
||||
return assembleActor(app, ctx, dbActor, includeFollows || dbActor.Id == "")
|
||||
}
|
||||
|
||||
func iriFromHandle(domain string, username string) (string, error) {
|
||||
client := &http.Client{}
|
||||
func iriFromHandle(ctx context.Context, domain string, username string) (string, error) {
|
||||
client := util.SafeHTTPClient()
|
||||
|
||||
webfingerURL := fmt.Sprintf("http://%s/.well-known/webfinger?resource=acct:%s@%s", domain, username, domain)
|
||||
resp, err := client.Get(webfingerURL)
|
||||
if err != nil || resp.StatusCode != http.StatusOK {
|
||||
return "", fmt.Errorf("webfinger request failed: %v", err)
|
||||
u := &url.URL{
|
||||
Scheme: "https",
|
||||
Host: domain,
|
||||
Path: "/.well-known/webfinger",
|
||||
}
|
||||
q := u.Query()
|
||||
q.Set("resource", fmt.Sprintf("acct:%s@%s", username, domain))
|
||||
u.RawQuery = q.Encode()
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", u.String(), nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("webfinger request failed: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return "", fmt.Errorf("unexpected status: %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
limitedReader := io.LimitReader(resp.Body, 102400)
|
||||
|
||||
var wf WebfingerResponse
|
||||
if err := json.NewDecoder(resp.Body).Decode(&wf); err != nil {
|
||||
return "", err
|
||||
if err := json.NewDecoder(limitedReader).Decode(&wf); err != nil {
|
||||
return "", fmt.Errorf("failed to decode JSON: %w", err)
|
||||
}
|
||||
|
||||
for _, link := range wf.Links {
|
||||
if link.Rel == "self" {
|
||||
if _, err := url.Parse(link.Href); err != nil {
|
||||
return "", fmt.Errorf("invalid IRI in response")
|
||||
}
|
||||
return link.Href, nil
|
||||
}
|
||||
}
|
||||
return "", fmt.Errorf("no iri in response")
|
||||
}
|
||||
|
||||
func assembleActor(actor *core.Record, dbActor *core.Record, app core.App, includeFollows bool) (*core.Record, error) {
|
||||
func assembleActor(app core.App, ctx context.Context, dbActor *core.Record, includeFollows bool) (*core.Record, error) {
|
||||
origin := os.Getenv("ORIGIN")
|
||||
if origin == "" {
|
||||
return nil, fmt.Errorf("ORIGIN environment variable not set")
|
||||
@@ -140,16 +181,16 @@ func assembleActor(actor *core.Record, dbActor *core.Record, app core.App, inclu
|
||||
dbActor.Set("icon", fmt.Sprintf("%s/api/v1/files/users/%s/%s", origin, user.Id, user.GetString("avatar")))
|
||||
}
|
||||
dbActor.Set("summary", settings.GetString("bio"))
|
||||
followerCount, err := app.CountRecords("follows", dbx.NewExp("followee={:user}", dbx.Params{"user": dbActor.Id}))
|
||||
followerCount, err := app.CountRecords("follows", dbx.NewExp("followee={:user} AND status='accepted'", dbx.Params{"user": dbActor.Id}))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
dbActor.Set("followerCount", followerCount)
|
||||
followingCount, err := app.CountRecords("follows", dbx.NewExp("follower={:user}", dbx.Params{"user": dbActor.Id}))
|
||||
dbActor.Set("follower_count", followerCount)
|
||||
followingCount, err := app.CountRecords("follows", dbx.NewExp("follower={:user} AND status='accepted'", dbx.Params{"user": dbActor.Id}))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
dbActor.Set("followingCount", followingCount)
|
||||
dbActor.Set("following_count", followingCount)
|
||||
|
||||
dbActor.Set("last_fetched", time.Now())
|
||||
|
||||
@@ -157,16 +198,17 @@ func assembleActor(actor *core.Record, dbActor *core.Record, app core.App, inclu
|
||||
result := make(map[string]interface{})
|
||||
json.Unmarshal([]byte(privacy), &result)
|
||||
|
||||
private = result["account"] == "private"
|
||||
// check that it's not our own profile
|
||||
private = result["account"] == "private" && dbActor.Id != strings.TrimPrefix(ctx.Value("actor").(string), "actor:")
|
||||
|
||||
} else {
|
||||
|
||||
// check if value is still cached
|
||||
twoHoursAgo := time.Now().Add(-2 * time.Hour)
|
||||
if !includeFollows && dbActor.GetDateTime("last_fetched").Time().After(twoHoursAgo) {
|
||||
twoHoursAgo := time.Now().UTC().Add(-2 * time.Hour)
|
||||
if dbActor.GetDateTime("last_fetched").Time().After(twoHoursAgo) {
|
||||
return dbActor, nil
|
||||
}
|
||||
pubActor, followers, following, err := fetchRemoteActor(actor, dbActor.GetString("iri"), includeFollows)
|
||||
pubActor, followers, following, err := fetchRemoteActor(app, ctx, dbActor.GetString("iri"), includeFollows)
|
||||
if err != nil {
|
||||
if dbActor.Id != "" {
|
||||
return dbActor, err
|
||||
@@ -188,53 +230,55 @@ func assembleActor(actor *core.Record, dbActor *core.Record, app core.App, inclu
|
||||
}
|
||||
domain := strings.TrimPrefix(parsedUrl.Hostname(), "www.")
|
||||
|
||||
// this is a race condition that gets triggered when the profile is opened for the first time
|
||||
existingActor, _ := app.FindFirstRecordByData("activitypub_actors", "iri", dbActor.GetString("iri"))
|
||||
|
||||
if existingActor != nil {
|
||||
dbActor = existingActor
|
||||
}
|
||||
|
||||
dbActor.Set("domain", domain)
|
||||
dbActor.Set("followers", pubActor.Followers.GetID().String())
|
||||
dbActor.Set("inbox", pubActor.Inbox.GetID().String())
|
||||
dbActor.Set("followers", util.ItemID(pubActor.Followers))
|
||||
dbActor.Set("inbox", util.ItemID(pubActor.Inbox))
|
||||
dbActor.Set("iri", pubActor.GetID().String())
|
||||
dbActor.Set("username", pubActor.Name.String())
|
||||
dbActor.Set("preferred_username", pubActor.PreferredUsername.String())
|
||||
dbActor.Set("following", pubActor.Following.GetID().String())
|
||||
dbActor.Set("following", util.ItemID(pubActor.Following))
|
||||
dbActor.Set("summary", pubActor.Summary.String())
|
||||
dbActor.Set("outbox", pubActor.Outbox.GetID().String())
|
||||
dbActor.Set("outbox", util.ItemID(pubActor.Outbox))
|
||||
dbActor.Set("icon", icon)
|
||||
dbActor.Set("published", pubActor.Published.String())
|
||||
dbActor.Set("public_key", pubActor.PublicKey.PublicKeyPem)
|
||||
dbActor.Set("last_fetched", time.Now())
|
||||
|
||||
if includeFollows {
|
||||
dbActor.Set("followerCount", int(followers.TotalItems))
|
||||
dbActor.Set("followingCount", int(following.TotalItems))
|
||||
dbActor.Set("follower_count", int(followers.TotalItems))
|
||||
dbActor.Set("following_count", int(following.TotalItems))
|
||||
}
|
||||
}
|
||||
|
||||
err := app.Save(dbActor)
|
||||
if err != nil && err.Error() == "iri: Value must be unique." {
|
||||
dbActor, err = app.FindFirstRecordByData("activitypub_actors", "iri", dbActor.GetString("iri"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return dbActor, nil
|
||||
} else if err != nil {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if private {
|
||||
return dbActor, fmt.Errorf("profile is private")
|
||||
return dbActor, ErrProfilePrivate
|
||||
}
|
||||
|
||||
return dbActor, nil
|
||||
}
|
||||
|
||||
// Fetches an AP actor and optionally followers/following collections
|
||||
func fetchRemoteActor(actor *core.Record, iri string, includeFollows bool) (*pub.Actor, *pub.OrderedCollection, *pub.OrderedCollection, error) {
|
||||
func fetchRemoteActor(app core.App, ctx context.Context, iri string, includeFollows bool) (*pub.Actor, *pub.OrderedCollection, *pub.OrderedCollection, error) {
|
||||
encryptionKey := os.Getenv("POCKETBASE_ENCRYPTION_KEY")
|
||||
if len(encryptionKey) == 0 {
|
||||
return nil, nil, nil, fmt.Errorf("POCKETBASE_ENCRYPTION_KEY not set")
|
||||
}
|
||||
|
||||
client := &http.Client{}
|
||||
req, _ := http.NewRequest("GET", iri, nil)
|
||||
client := util.SafeHTTPClient()
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", iri, nil)
|
||||
|
||||
headers := map[string]string{
|
||||
"Accept": "application/ld+json",
|
||||
@@ -247,8 +291,11 @@ func fetchRemoteActor(actor *core.Record, iri string, includeFollows bool) (*pub
|
||||
req.Header.Add(k, v)
|
||||
}
|
||||
|
||||
dbPrivateKey := actor.GetString("private_key")
|
||||
if dbPrivateKey != "" {
|
||||
userActorId := strings.TrimPrefix(ctx.Value("actor").(string), "actor:")
|
||||
userActor, err := app.FindRecordById("activitypub_actors", userActorId)
|
||||
if userActor != nil && userActor.GetString("private_key") != "" {
|
||||
dbPrivateKey := userActor.GetString("private_key")
|
||||
|
||||
algs := []httpsig.Algorithm{httpsig.RSA_SHA256}
|
||||
postHeaders := []string{"(request-target)", "Date", "Digest", "Content-Type", "Host"}
|
||||
expiresIn := 60
|
||||
@@ -267,7 +314,7 @@ func fetchRemoteActor(actor *core.Record, iri string, includeFollows bool) (*pub
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
|
||||
pubID := actor.GetString("iri") + "#main-key"
|
||||
pubID := userActor.GetString("iri") + "#main-key"
|
||||
|
||||
if err := signer.SignRequest(privateKey, pubID, req, []byte{}); err != nil {
|
||||
return nil, nil, nil, err
|
||||
@@ -289,16 +336,21 @@ func fetchRemoteActor(actor *core.Record, iri string, includeFollows bool) (*pub
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
|
||||
// Validate actor response has required fields
|
||||
if err := validateActorResponse(&pubActor); err != nil {
|
||||
return nil, nil, nil, fmt.Errorf("actor validation failed for %s: %w", iri, err)
|
||||
}
|
||||
|
||||
var followers, following pub.OrderedCollection
|
||||
|
||||
if includeFollows {
|
||||
// Fetch followers
|
||||
if data, err := FetchCollection(actor, pubActor.Followers.GetID().String()); err == nil {
|
||||
if data, err := FetchCollection(app, ctx, util.ItemID(pubActor.Followers)); err == nil {
|
||||
followers = *data
|
||||
}
|
||||
|
||||
// Fetch following
|
||||
if data, err := FetchCollection(actor, pubActor.Following.GetID().String()); err == nil {
|
||||
if data, err := FetchCollection(app, ctx, util.ItemID(pubActor.Following)); err == nil {
|
||||
following = *data
|
||||
}
|
||||
}
|
||||
@@ -306,13 +358,13 @@ func fetchRemoteActor(actor *core.Record, iri string, includeFollows bool) (*pub
|
||||
return &pubActor, &followers, &following, nil
|
||||
}
|
||||
|
||||
func FetchCollection(actor *core.Record, url string) (*pub.OrderedCollection, error) {
|
||||
func FetchCollection(app core.App, ctx context.Context, collectionURL string) (*pub.OrderedCollection, error) {
|
||||
encryptionKey := os.Getenv("POCKETBASE_ENCRYPTION_KEY")
|
||||
if len(encryptionKey) == 0 {
|
||||
return nil, fmt.Errorf("POCKETBASE_ENCRYPTION_KEY not set")
|
||||
}
|
||||
|
||||
req, _ := http.NewRequest("GET", url, nil)
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", collectionURL, nil)
|
||||
|
||||
headers := map[string]string{
|
||||
"Accept": "application/ld+json",
|
||||
@@ -324,38 +376,48 @@ func FetchCollection(actor *core.Record, url string) (*pub.OrderedCollection, er
|
||||
for k, v := range headers {
|
||||
req.Header.Add(k, v)
|
||||
}
|
||||
userActorId := strings.TrimPrefix(ctx.Value("actor").(string), "actor:")
|
||||
userActor, err := app.FindRecordById("activitypub_actors", userActorId)
|
||||
if userActor != nil && userActor.GetString("private_key") != "" {
|
||||
dbPrivateKey := userActor.GetString("private_key")
|
||||
if dbPrivateKey != "" {
|
||||
algs := []httpsig.Algorithm{httpsig.RSA_SHA256}
|
||||
postHeaders := []string{"(request-target)", "Date", "Digest", "Content-Type", "Host"}
|
||||
expiresIn := 60
|
||||
|
||||
dbPrivateKey := actor.GetString("private_key")
|
||||
if dbPrivateKey != "" {
|
||||
algs := []httpsig.Algorithm{httpsig.RSA_SHA256}
|
||||
postHeaders := []string{"(request-target)", "Date", "Digest", "Content-Type", "Host"}
|
||||
expiresIn := 60
|
||||
signer, _, err := httpsig.NewSigner(algs, httpsig.DigestSha256, postHeaders, httpsig.Signature, int64(expiresIn))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
decryptedPrivateKey, err := security.Decrypt(dbPrivateKey, encryptionKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
privateKey, err := x509.ParsePKCS1PrivateKey(decryptedPrivateKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
pubID := userActor.GetString("iri") + "#main-key"
|
||||
|
||||
if err := signer.SignRequest(privateKey, pubID, req, []byte{}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
signer, _, err := httpsig.NewSigner(algs, httpsig.DigestSha256, postHeaders, httpsig.Signature, int64(expiresIn))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
decryptedPrivateKey, err := security.Decrypt(dbPrivateKey, encryptionKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
privateKey, err := x509.ParsePKCS1PrivateKey(decryptedPrivateKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
pubID := actor.GetString("iri") + "#main-key"
|
||||
|
||||
if err := signer.SignRequest(privateKey, pubID, req, []byte{}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil || resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("collection fetch failed for %s: %v", url, err)
|
||||
client := util.SafeHTTPClient()
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("collection fetch failed for %s: %v", collectionURL, err)
|
||||
}
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
if resp.StatusCode == http.StatusNotFound {
|
||||
return nil, ErrProfilePrivate
|
||||
}
|
||||
return nil, fmt.Errorf("collection fetch %s returned: %v", collectionURL, resp.StatusCode)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ import (
|
||||
"golang.org/x/net/html"
|
||||
)
|
||||
|
||||
func CreateTrailActivity(app core.App, actor *core.Record, trail *core.Record, typ pub.ActivityVocabularyType) error {
|
||||
func CreateTrailActivity(app core.App, ctx context.Context, trail *core.Record, typ pub.ActivityVocabularyType) error {
|
||||
if !trail.GetBool("public") {
|
||||
// only broadcast the trail if it is public
|
||||
return nil
|
||||
@@ -46,7 +46,7 @@ func CreateTrailActivity(app core.App, actor *core.Record, trail *core.Record, t
|
||||
id := fmt.Sprintf("%s/api/v1/activitypub/activity/%s", origin, recordId)
|
||||
to := "https://www.w3.org/ns/activitystreams#Public"
|
||||
|
||||
mentionedActors, handles, err := ActorsFromMentions(app, actor, trail.GetString("description"))
|
||||
mentionedActors, err := ActorsFromMentions(app, ctx, trail.GetString("description"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -54,11 +54,11 @@ func CreateTrailActivity(app core.App, actor *core.Record, trail *core.Record, t
|
||||
mentions := []string{}
|
||||
cc := pub.ItemCollection{pub.IRI(trailAuthor.GetString("followers"))}
|
||||
tags := pub.ItemCollection{}
|
||||
for i, m := range mentionedActors {
|
||||
for _, m := range mentionedActors {
|
||||
inbox := m.GetString("inbox")
|
||||
mention := pub.MentionNew(pub.IRI(m.GetString("iri")))
|
||||
mention.Href = pub.IRI(m.GetString("iri"))
|
||||
mention.Name = pub.NaturalLanguageValuesNew(pub.LangRefValueNew(pub.NilLangRef, handles[i]))
|
||||
mention.Name = pub.NaturalLanguageValuesNew(pub.LangRefValueNew(pub.NilLangRef, fmt.Sprintf("@%s@%s", m.GetString("preferred_username"), m.GetString("domain"))))
|
||||
tags.Append(mention)
|
||||
|
||||
mentions = append(mentions, inbox)
|
||||
@@ -108,7 +108,7 @@ func CreateTrailActivity(app core.App, actor *core.Record, trail *core.Record, t
|
||||
return PostActivity(app, trailAuthor, activity, recipients)
|
||||
}
|
||||
|
||||
func CreateCommentActivity(app core.App, actor *core.Record, comment *core.Record, typ pub.ActivityVocabularyType) error {
|
||||
func CreateCommentActivity(app core.App, ctx context.Context, comment *core.Record, typ pub.ActivityVocabularyType) error {
|
||||
origin := os.Getenv("ORIGIN")
|
||||
if origin == "" {
|
||||
return fmt.Errorf("ORIGIN not set")
|
||||
@@ -134,16 +134,16 @@ func CreateCommentActivity(app core.App, actor *core.Record, comment *core.Recor
|
||||
id := fmt.Sprintf("%s/api/v1/activitypub/activity/%s", origin, activityRecordId)
|
||||
to := "https://www.w3.org/ns/activitystreams#Public"
|
||||
|
||||
mentionedActors, handles, err := ActorsFromMentions(app, actor, comment.GetString("text"))
|
||||
mentionedActors, err := ActorsFromMentions(app, ctx, comment.GetString("text"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
recipients := []string{}
|
||||
tags := pub.ItemCollection{}
|
||||
for i, m := range mentionedActors {
|
||||
for _, m := range mentionedActors {
|
||||
mention := pub.MentionNew(pub.IRI(m.GetString("iri")))
|
||||
mention.Href = pub.IRI(m.GetString("iri"))
|
||||
mention.Name = pub.NaturalLanguageValuesNew(pub.LangRefValueNew(pub.NilLangRef, handles[i]))
|
||||
mention.Name = pub.NaturalLanguageValuesNew(pub.LangRefValueNew(pub.NilLangRef, fmt.Sprintf("@%s@%s", m.GetString("preferred_username"), m.GetString("domain"))))
|
||||
tags.Append(mention)
|
||||
|
||||
recipients = append(recipients, m.GetString("inbox"))
|
||||
@@ -193,7 +193,7 @@ func CreateCommentActivity(app core.App, actor *core.Record, comment *core.Recor
|
||||
|
||||
}
|
||||
|
||||
func CreateSummitLogActivity(app core.App, actor *core.Record, summitLog *core.Record, typ pub.ActivityVocabularyType) error {
|
||||
func CreateSummitLogActivity(app core.App, ctx context.Context, summitLog *core.Record, typ pub.ActivityVocabularyType) error {
|
||||
|
||||
origin := os.Getenv("ORIGIN")
|
||||
if origin == "" {
|
||||
@@ -245,7 +245,7 @@ func CreateSummitLogActivity(app core.App, actor *core.Record, summitLog *core.R
|
||||
to.Append(pub.IRI(summitLogTrailAuthor.GetString("iri")))
|
||||
}
|
||||
|
||||
mentionedActors, handles, err := ActorsFromMentions(app, actor, summitLog.GetString("text"))
|
||||
mentionedActors, err := ActorsFromMentions(app, ctx, summitLog.GetString("text"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -253,11 +253,11 @@ func CreateSummitLogActivity(app core.App, actor *core.Record, summitLog *core.R
|
||||
mentions := []string{}
|
||||
cc := pub.ItemCollection{pub.IRI(summitLogAuthor.GetString("followers"))}
|
||||
mentionTags := pub.ItemCollection{}
|
||||
for i, m := range mentionedActors {
|
||||
for _, m := range mentionedActors {
|
||||
inbox := m.GetString("inbox")
|
||||
mention := pub.MentionNew(pub.IRI(m.GetString("iri")))
|
||||
mention.Href = pub.IRI(m.GetString("iri"))
|
||||
mention.Name = pub.NaturalLanguageValuesNew(pub.LangRefValueNew(pub.NilLangRef, handles[i]))
|
||||
mention.Name = pub.NaturalLanguageValuesNew(pub.LangRefValueNew(pub.NilLangRef, fmt.Sprintf("@%s@%s", m.GetString("preferred_username"), m.GetString("domain"))))
|
||||
mentionTags.Append(mention)
|
||||
|
||||
mentions = append(mentions, inbox)
|
||||
@@ -444,15 +444,15 @@ func CreateListActivity(app core.App, list *core.Record, typ pub.ActivityVocabul
|
||||
return app.Save(record)
|
||||
}
|
||||
|
||||
func ProcessCreateOrUpdateActivity(app core.App, actor *core.Record, activity pub.Activity) error {
|
||||
func ProcessCreateOrUpdateActivity(app core.App, actor *core.Record, recipient *core.Record, activity pub.Activity) error {
|
||||
|
||||
var err error
|
||||
if strings.Contains(activity.Object.GetID().String(), "/api/v1/trail") {
|
||||
err = processCreateOrUpdateTrailActivity(activity, app, actor)
|
||||
err = processCreateOrUpdateTrailActivity(activity, app, actor, recipient)
|
||||
} else if strings.Contains(activity.Object.GetID().String(), "/api/v1/summit-log") {
|
||||
err = processCreateOrUpdateSummitLogActivity(activity, app, actor)
|
||||
} else if strings.Contains(activity.Object.GetID().String(), "/api/v1/list") {
|
||||
err = processCreateOrUpdateListActivity(activity, app, actor)
|
||||
err = processCreateOrUpdateListActivity(activity, app, actor, recipient)
|
||||
} else {
|
||||
err = processCreateOrUpdateCommentActivity(activity, app, actor)
|
||||
}
|
||||
@@ -465,14 +465,16 @@ func ProcessCreateOrUpdateActivity(app core.App, actor *core.Record, activity pu
|
||||
|
||||
}
|
||||
|
||||
func processCreateOrUpdateTrailActivity(activity pub.Activity, app core.App, actor *core.Record) error {
|
||||
|
||||
// no need to do anything if the actor is local
|
||||
if actor.GetBool("isLocal") {
|
||||
return nil
|
||||
func processCreateOrUpdateTrailActivity(activity pub.Activity, app core.App, actor *core.Record, recipient *core.Record) error {
|
||||
trail, err := util.TrailFromActivity(activity, app, actor)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
trail, err := util.TrailFromActivity(activity, app, actor)
|
||||
_, err = util.InsertIntoFeed(app, recipient.Id, actor.Id, trail.Id, util.TrailFeed)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
trailObject, _ := pub.ToObject(activity.Object)
|
||||
|
||||
@@ -791,22 +793,24 @@ func processCreateOrUpdateSummitLogActivity(activity pub.Activity, app core.App,
|
||||
return nil
|
||||
}
|
||||
|
||||
func processCreateOrUpdateListActivity(activity pub.Activity, app core.App, actor *core.Record) error {
|
||||
|
||||
// no need to do anything if the actor is local
|
||||
if actor.GetBool("isLocal") {
|
||||
return nil
|
||||
func processCreateOrUpdateListActivity(activity pub.Activity, app core.App, actor *core.Record, recipient *core.Record) error {
|
||||
list, err := util.ListFromActivity(activity, app, actor)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err := util.ListFromActivity(activity, app, actor)
|
||||
_, err = util.InsertIntoFeed(app, recipient.Id, actor.Id, list.Id, util.ListFeed)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func ActorsFromMentions(app core.App, actor *core.Record, htmlStr string) ([]*core.Record, []string, error) {
|
||||
func ActorsFromMentions(app core.App, ctx context.Context, htmlStr string) ([]*core.Record, error) {
|
||||
doc, err := html.Parse(strings.NewReader(htmlStr))
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var handles []string
|
||||
@@ -838,12 +842,12 @@ func ActorsFromMentions(app core.App, actor *core.Record, htmlStr string) ([]*co
|
||||
f(doc)
|
||||
|
||||
for _, h := range handles {
|
||||
actor, err := GetActorByHandle(app, actor, h, false)
|
||||
actor, err := GetActorByHandle(app, ctx, h, false)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
actors = append(actors, actor)
|
||||
}
|
||||
|
||||
return actors, handles, nil
|
||||
return actors, nil
|
||||
}
|
||||
|
||||
@@ -2,9 +2,8 @@ package federation
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"os"
|
||||
"path"
|
||||
"pocketbase/util"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -16,7 +15,10 @@ import (
|
||||
)
|
||||
|
||||
func CreateTrailDeleteActivity(app core.App, r *core.Record) error {
|
||||
|
||||
if !r.GetBool("public") {
|
||||
// only broadcast the trail if it is public
|
||||
return nil
|
||||
}
|
||||
origin := os.Getenv("ORIGIN")
|
||||
if origin == "" {
|
||||
return fmt.Errorf("ORIGIN not set")
|
||||
@@ -315,16 +317,17 @@ func ProcessDeleteActivity(app core.App, actor *core.Record, activity pub.Activi
|
||||
|
||||
func processDeleteTrailActivity(app core.App, activity pub.Activity) error {
|
||||
|
||||
trailUrl, err := url.Parse(activity.Object.GetID().String())
|
||||
object := activity.Object.GetID().String()
|
||||
trail, err := app.FindFirstRecordByData("trails", "iri", object)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
recordId := path.Base(trailUrl.Path)
|
||||
|
||||
trail, err := app.FindRecordById("trails", recordId)
|
||||
err = util.DeleteFromFeed(app, trail.Id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return app.Delete(trail)
|
||||
}
|
||||
|
||||
@@ -370,8 +373,10 @@ func processDeleteListActivity(app core.App, actor *core.Record, activity pub.Ac
|
||||
return err
|
||||
}
|
||||
|
||||
if list.GetString("author") != actor.Id {
|
||||
return fmt.Errorf("actor is not summit log author")
|
||||
err = util.DeleteFromFeed(app, list.Id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return app.Delete(list)
|
||||
}
|
||||
|
||||
68
db/go.mod
68
db/go.mod
@@ -1,64 +1,58 @@
|
||||
module pocketbase
|
||||
|
||||
go 1.23.0
|
||||
|
||||
toolchain go1.24.1
|
||||
go 1.25.0
|
||||
|
||||
require (
|
||||
github.com/meilisearch/meilisearch-go v0.29.0
|
||||
github.com/pocketbase/dbx v1.11.0
|
||||
github.com/pocketbase/pocketbase v0.26.1
|
||||
github.com/go-ap/jsonld v0.0.0-20250905102310-8480b0fe24d9
|
||||
github.com/meilisearch/meilisearch-go v0.36.2
|
||||
github.com/pocketbase/dbx v1.12.0
|
||||
github.com/pocketbase/pocketbase v0.38.0
|
||||
github.com/tkrajina/gpxgo v1.4.0
|
||||
)
|
||||
|
||||
require (
|
||||
git.sr.ht/~mariusor/go-xsd-duration v0.0.0-20220703122237-02e73435a078 // indirect
|
||||
github.com/aymerick/douceur v0.2.0 // indirect
|
||||
github.com/go-ap/errors v0.0.0-20250409143711-5686c11ae650 // indirect
|
||||
github.com/go-ap/jsonld v0.0.0-20221030091449-f2a191312c73 // indirect
|
||||
github.com/go-sql-driver/mysql v1.8.1 // indirect
|
||||
github.com/golang-jwt/jwt/v5 v5.2.1 // indirect
|
||||
github.com/fsnotify/fsnotify v1.7.0 // indirect
|
||||
github.com/go-ap/errors v0.0.0-20250905102357-4480b47a00c4 // indirect
|
||||
github.com/go-sql-driver/mysql v1.9.3 // indirect
|
||||
github.com/golang-jwt/jwt/v5 v5.3.1 // indirect
|
||||
github.com/gorilla/css v1.0.1 // indirect
|
||||
github.com/twpayne/go-geom v1.6.1 // indirect
|
||||
github.com/valyala/fastjson v1.6.4 // indirect
|
||||
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 // indirect
|
||||
github.com/valyala/fastjson v1.6.10 // indirect
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/andybalholm/brotli v1.1.0 // indirect
|
||||
github.com/andybalholm/brotli v1.2.0 // indirect
|
||||
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect
|
||||
github.com/disintegration/imaging v1.6.2 // indirect
|
||||
github.com/domodwyer/mailyak/v3 v3.6.2 // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/fatih/color v1.18.0 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.8 // indirect
|
||||
github.com/ganigeorgiev/fexpr v0.4.1 // indirect
|
||||
github.com/go-ap/activitypub v0.0.0-20250409143848-7113328b1f3d
|
||||
github.com/fatih/color v1.19.0 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.13 // indirect
|
||||
github.com/ganigeorgiev/fexpr v0.5.0 // indirect
|
||||
github.com/go-ap/activitypub v0.0.0-20250905102448-e9df599e4528
|
||||
github.com/go-fed/httpsig v1.1.0
|
||||
github.com/go-ozzo/ozzo-validation/v4 v4.3.0 // indirect
|
||||
github.com/golang-jwt/jwt/v4 v4.5.1 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/mailru/easyjson v0.7.7 // indirect
|
||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mattn/go-isatty v0.0.22 // indirect
|
||||
github.com/microcosm-cc/bluemonday v1.0.27
|
||||
github.com/ncruces/go-strftime v0.1.9 // indirect
|
||||
github.com/ncruces/go-strftime v1.0.0 // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||
github.com/spf13/cast v1.7.1
|
||||
github.com/spf13/cobra v1.9.1 // indirect
|
||||
github.com/spf13/pflag v1.0.6 // indirect
|
||||
github.com/twpayne/go-gpx v1.5.0
|
||||
github.com/spf13/cast v1.10.0
|
||||
github.com/spf13/cobra v1.10.2
|
||||
github.com/spf13/pflag v1.0.10 // indirect
|
||||
github.com/twpayne/go-polyline v1.1.1
|
||||
golang.org/x/crypto v0.37.0 // indirect
|
||||
golang.org/x/image v0.25.0 // indirect
|
||||
golang.org/x/net v0.39.0 // indirect
|
||||
golang.org/x/oauth2 v0.28.0 // indirect
|
||||
golang.org/x/sync v0.13.0 // indirect
|
||||
golang.org/x/sys v0.32.0 // indirect
|
||||
golang.org/x/text v0.24.0 // indirect
|
||||
modernc.org/libc v1.61.13 // indirect
|
||||
golang.org/x/crypto v0.50.0 // indirect
|
||||
golang.org/x/image v0.39.0 // indirect
|
||||
golang.org/x/net v0.53.0
|
||||
golang.org/x/oauth2 v0.36.0 // indirect
|
||||
golang.org/x/sync v0.20.0
|
||||
golang.org/x/sys v0.43.0 // indirect
|
||||
golang.org/x/text v0.36.0 // indirect
|
||||
modernc.org/libc v1.72.0 // indirect
|
||||
modernc.org/mathutil v1.7.1 // indirect
|
||||
modernc.org/memory v1.8.2 // indirect
|
||||
modernc.org/sqlite v1.36.1 // indirect
|
||||
modernc.org/memory v1.11.0 // indirect
|
||||
modernc.org/sqlite v1.50.0 // indirect
|
||||
)
|
||||
|
||||
197
db/go.sum
197
db/go.sum
@@ -2,12 +2,8 @@ filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
||||
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
||||
git.sr.ht/~mariusor/go-xsd-duration v0.0.0-20220703122237-02e73435a078 h1:cliQ4HHsCo6xi2oWZYKWW4bly/Ory9FuTpFPRxj/mAg=
|
||||
git.sr.ht/~mariusor/go-xsd-duration v0.0.0-20220703122237-02e73435a078/go.mod h1:g/V2Hjas6Z1UHUp4yIx6bATpNzJ7DYtD0FG3+xARWxs=
|
||||
github.com/alecthomas/assert/v2 v2.10.0 h1:jjRCHsj6hBJhkmhznrCzoNpbA3zqy0fYiUcYZP/GkPY=
|
||||
github.com/alecthomas/assert/v2 v2.10.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
|
||||
github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc=
|
||||
github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
|
||||
github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
|
||||
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
|
||||
github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=
|
||||
github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
|
||||
github.com/asaskevich/govalidator v0.0.0-20200108200545-475eaeb16496/go.mod h1:oGkLhpf+kjZl6xBf758TQhh5XrAeiJv/7FRz/2spLIg=
|
||||
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so=
|
||||
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw=
|
||||
@@ -24,130 +20,127 @@ github.com/domodwyer/mailyak/v3 v3.6.2/go.mod h1:lOm/u9CyCVWHeaAmHIdF4RiKVxKUT/H
|
||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
github.com/dvyukov/go-fuzz v0.0.0-20200318091601-be3528f3a813/go.mod h1:11Gm+ccJnvAhCNLlf5+cS9KjtbaD5I5zaZpFMsTHWTw=
|
||||
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
|
||||
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
|
||||
github.com/fatih/color v1.19.0 h1:Zp3PiM21/9Ld6FzSKyL5c/BULoe/ONr9KlbYVOfG8+w=
|
||||
github.com/fatih/color v1.19.0/go.mod h1:zNk67I0ZUT1bEGsSGyCZYZNrHuTkJJB+r6Q9VuMi0LE=
|
||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||
github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
|
||||
github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8=
|
||||
github.com/ganigeorgiev/fexpr v0.4.1 h1:hpUgbUEEWIZhSDBtf4M9aUNfQQ0BZkGRaMePy7Gcx5k=
|
||||
github.com/ganigeorgiev/fexpr v0.4.1/go.mod h1:RyGiGqmeXhEQ6+mlGdnUleLHgtzzu/VGO2WtJkF5drE=
|
||||
github.com/go-ap/activitypub v0.0.0-20250409143848-7113328b1f3d h1:IWrWGnmKzpHqginJ18ljKkty/X8glxM8Mg3pk6bkb8g=
|
||||
github.com/go-ap/activitypub v0.0.0-20250409143848-7113328b1f3d/go.mod h1:EUtZuXtHo4yKkTJmcbAZYW+X1G2poeT8icmBh24eq7o=
|
||||
github.com/go-ap/errors v0.0.0-20250409143711-5686c11ae650 h1:tlwla5IQUea0CuktkBd2FLDwVzts4OeTWPPkhQPSK5Q=
|
||||
github.com/go-ap/errors v0.0.0-20250409143711-5686c11ae650/go.mod h1:Vkh+Z3f24K8nMsJKXo1FHn5ebPsXvB/WDH5JRtYqdNo=
|
||||
github.com/go-ap/jsonld v0.0.0-20221030091449-f2a191312c73 h1:GMKIYXyXPGIp+hYiWOhfqK4A023HdgisDT4YGgf99mw=
|
||||
github.com/go-ap/jsonld v0.0.0-20221030091449-f2a191312c73/go.mod h1:jyveZeGw5LaADntW+UEsMjl3IlIwk+DxlYNsbofQkGA=
|
||||
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
|
||||
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
|
||||
github.com/gabriel-vasile/mimetype v1.4.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM=
|
||||
github.com/gabriel-vasile/mimetype v1.4.13/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
|
||||
github.com/ganigeorgiev/fexpr v0.5.0 h1:XA9JxtTE/Xm+g/JFI6RfZEHSiQlk+1glLvRK1Lpv/Tk=
|
||||
github.com/ganigeorgiev/fexpr v0.5.0/go.mod h1:RyGiGqmeXhEQ6+mlGdnUleLHgtzzu/VGO2WtJkF5drE=
|
||||
github.com/go-ap/activitypub v0.0.0-20250905102448-e9df599e4528 h1:6CyCdRGY7rwTUjLseGcFkCwEYUYNOghvNMzxXROjU00=
|
||||
github.com/go-ap/activitypub v0.0.0-20250905102448-e9df599e4528/go.mod h1:3ek8fXe976305mQAuuuRJgTZy4hmv0TSPWtmUYdGVfw=
|
||||
github.com/go-ap/errors v0.0.0-20250905102357-4480b47a00c4 h1:Kpa0XF3gCAdeRSFzliN9jgg35KvsCchv3KRCRIJhi6s=
|
||||
github.com/go-ap/errors v0.0.0-20250905102357-4480b47a00c4/go.mod h1:qHHr/m8ECbV2lkT1j5VaNnauKFV1nUo+i8S53Wtrb0A=
|
||||
github.com/go-ap/jsonld v0.0.0-20250905102310-8480b0fe24d9 h1:gaBrU/E+usPHIafDIC2EwvZbehvgAEuu78Jk0zjxw5w=
|
||||
github.com/go-ap/jsonld v0.0.0-20250905102310-8480b0fe24d9/go.mod h1:4h93IBxgfnE/DEleMLgJ/XCeu/RtQ+MUh3ucANseeXA=
|
||||
github.com/go-fed/httpsig v1.1.0 h1:9M+hb0jkEICD8/cAiNqEB66R87tTINszBRTjwjQzWcI=
|
||||
github.com/go-fed/httpsig v1.1.0/go.mod h1:RCMrTZvN1bJYtofsG4rd5NaO5obxQ5xBkdiS7xsT7bM=
|
||||
github.com/go-ozzo/ozzo-validation/v4 v4.3.0 h1:byhDUpfEwjsVQb1vBunvIjh2BHQ9ead57VkAEY4V+Es=
|
||||
github.com/go-ozzo/ozzo-validation/v4 v4.3.0/go.mod h1:2NKgrcHl3z6cJs+3Oo940FPRiTzuqKbvfrL2RxCj6Ew=
|
||||
github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
|
||||
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
|
||||
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
|
||||
github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
|
||||
github.com/golang-jwt/jwt/v4 v4.5.1 h1:JdqV9zKUdtaa9gdPlywC3aeoEsR681PlKC+4F5gQgeo=
|
||||
github.com/golang-jwt/jwt/v4 v4.5.1/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||
github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo=
|
||||
github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
|
||||
github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY=
|
||||
github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
|
||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/pprof v0.0.0-20250315033105-103756e64e1d h1:tx51Lf+wdE+aavqH8TcPJoCjTf4cE8hrMzROghCely0=
|
||||
github.com/google/pprof v0.0.0-20250315033105-103756e64e1d/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144=
|
||||
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/pprof v0.0.0-20260402051712-545e8a4df936 h1:EwtI+Al+DeppwYX2oXJCETMO23COyaKGP6fHVpkpWpg=
|
||||
github.com/google/pprof v0.0.0-20260402051712-545e8a4df936/go.mod h1:MxpfABSjhmINe3F1It9d+8exIHFvUqtLIRCdOGNXqiI=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8=
|
||||
github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0=
|
||||
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
|
||||
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
|
||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
||||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
|
||||
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
||||
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
||||
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/meilisearch/meilisearch-go v0.29.0 h1:HZ9NEKN59USINQ/DXJge/aaXq8IrsKbXGTdAoBaaDz4=
|
||||
github.com/meilisearch/meilisearch-go v0.29.0/go.mod h1:2cRCAn4ddySUsFfNDLVPod/plRibQsJkXF/4gLhxbOk=
|
||||
github.com/mattn/go-isatty v0.0.22 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw4=
|
||||
github.com/mattn/go-isatty v0.0.22/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4=
|
||||
github.com/meilisearch/meilisearch-go v0.36.2 h1:MYaMPCpdLh2aYPt+zK+19mLoA4dfBY3S1L7T0FADCjU=
|
||||
github.com/meilisearch/meilisearch-go v0.36.2/go.mod h1:hWcR0MuWLSzHfbz9GGzIr3s9rnXLm1jqkmHkJPbUSvM=
|
||||
github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk=
|
||||
github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA=
|
||||
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
|
||||
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
||||
github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
|
||||
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/pocketbase/dbx v1.11.0 h1:LpZezioMfT3K4tLrqA55wWFw1EtH1pM4tzSVa7kgszU=
|
||||
github.com/pocketbase/dbx v1.11.0/go.mod h1:xXRCIAKTHMgUCyCKZm55pUOdvFziJjQfXaWKhu2vhMs=
|
||||
github.com/pocketbase/pocketbase v0.26.1 h1:0WBqIRKKPCqp+xHPVLB4fevkoT9HVlR4BSuNwAt5oJ0=
|
||||
github.com/pocketbase/pocketbase v0.26.1/go.mod h1:t5y5pfnhrEg//RuSzSg0a926OLZ0oQj66jYs3BzDJwA=
|
||||
github.com/pocketbase/dbx v1.12.0 h1:/oLErM+A0b4xI0PWTGPqSDVjzix48PqI/bng2l0PzoA=
|
||||
github.com/pocketbase/dbx v1.12.0/go.mod h1:xXRCIAKTHMgUCyCKZm55pUOdvFziJjQfXaWKhu2vhMs=
|
||||
github.com/pocketbase/pocketbase v0.38.0 h1:EwZiOpu2RwJ7O0d0+W+rUAGeXvBhH6zpZOX37wFB830=
|
||||
github.com/pocketbase/pocketbase v0.38.0/go.mod h1:gxdfarbZ4gT/ivRNiIa7uJ1a64eY8yncW+NyvA31aLk=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
|
||||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y=
|
||||
github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
|
||||
github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
|
||||
github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
|
||||
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
|
||||
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=
|
||||
github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=
|
||||
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
|
||||
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
|
||||
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
|
||||
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.3.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=
|
||||
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/twpayne/go-geom v1.6.1 h1:iLE+Opv0Ihm/ABIcvQFGIiFBXd76oBIar9drAwHFhR4=
|
||||
github.com/twpayne/go-geom v1.6.1/go.mod h1:Kr+Nly6BswFsKM5sd31YaoWS5PeDDH2NftJTK7Gd028=
|
||||
github.com/twpayne/go-gpx v1.5.0 h1:HvFSJ+0r0sbhOQ8mTvd0/n0FhcgjTFsKQGG6o7PV6G4=
|
||||
github.com/twpayne/go-gpx v1.5.0/go.mod h1:vjvu/125399qj6k+px2v2v8dm08DM4I4dFBJmHHt2TE=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/tkrajina/gpxgo v1.4.0 h1:cSD5uSwy3VZuNFieTEZLyRnuIwhonQEkGPkPGW4XNag=
|
||||
github.com/tkrajina/gpxgo v1.4.0/go.mod h1:BXSMfUAvKiEhMEXAFM2NvNsbjsSvp394mOvdcNjettg=
|
||||
github.com/twpayne/go-polyline v1.1.1 h1:/tSF1BR7rN4HWj4XKqvRUNrCiYVMCvywxTFVofvDV0w=
|
||||
github.com/twpayne/go-polyline v1.1.1/go.mod h1:ybd9IWWivW/rlXPXuuckeKUyF3yrIim+iqA7kSl4NFY=
|
||||
github.com/valyala/fastjson v1.6.4 h1:uAUNq9Z6ymTgGhcm0UynUAB6tlbakBrz6CQFax3BXVQ=
|
||||
github.com/valyala/fastjson v1.6.4/go.mod h1:CLCAqky6SMuOcxStkYQvblddUtoRxhYMGLrsQns1aXY=
|
||||
github.com/valyala/fastjson v1.6.10 h1:/yjJg8jaVQdYR3arGxPE2X5z89xrlhS0eGXdv+ADTh4=
|
||||
github.com/valyala/fastjson v1.6.10/go.mod h1:e6FubmQouUNP73jtMLmcbxS6ydWIpOfhz34TSfO3JaE=
|
||||
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
|
||||
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE=
|
||||
golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc=
|
||||
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 h1:nDVHiLt8aIbd/VzvPWN6kSOPE7+F/fNFDSXLVYkE/Iw=
|
||||
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394/go.mod h1:sIifuuw/Yco/y6yb6+bDNfyeQ/MdPUy/hKEMYQV17cM=
|
||||
golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI=
|
||||
golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q=
|
||||
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/image v0.25.0 h1:Y6uW6rH1y5y/LK1J8BPWZtr6yZ7hrsy6hFrXjgsc2fQ=
|
||||
golang.org/x/image v0.25.0/go.mod h1:tCAmOEGthTtkalusGp1g3xa2gke8J6c2N565dTyl9Rs=
|
||||
golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU=
|
||||
golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
|
||||
golang.org/x/image v0.39.0 h1:skVYidAEVKgn8lZ602XO75asgXBgLj9G/FE3RbuPFww=
|
||||
golang.org/x/image v0.39.0/go.mod h1:sIbmppfU+xFLPIG0FoVUTvyBMmgng1/XAMhQ2ft0hpA=
|
||||
golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI=
|
||||
golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
||||
golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY=
|
||||
golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E=
|
||||
golang.org/x/oauth2 v0.28.0 h1:CrgCKl8PPAVtLnU3c+EDw6x11699EWlsDeWNWKdIOkc=
|
||||
golang.org/x/oauth2 v0.28.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8=
|
||||
golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610=
|
||||
golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA=
|
||||
golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs=
|
||||
golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
|
||||
golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
|
||||
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
||||
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
|
||||
golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=
|
||||
golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.42.0 h1:UiKe+zDFmJobeJ5ggPwOshJIVt6/Ft0rcfrXZDLWAWY=
|
||||
golang.org/x/term v0.42.0/go.mod h1:Dq/D+snpsbazcBG5+F9Q1n2rXV8Ma+71xEjTRufARgY=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
|
||||
golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg=
|
||||
golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.31.0 h1:0EedkvKDbh+qistFTd0Bcwe/YLh4vHwWEkiI0toFIBU=
|
||||
golang.org/x/tools v0.31.0/go.mod h1:naFTU+Cev749tSJRXJlna0T3WxKvb1kWEx15xA4SdmQ=
|
||||
golang.org/x/tools v0.43.0 h1:12BdW9CeB3Z+J/I/wj34VMl8X+fEXBxVR90JeMX5E7s=
|
||||
golang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0=
|
||||
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
@@ -155,26 +148,30 @@ gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
modernc.org/cc/v4 v4.24.4 h1:TFkx1s6dCkQpd6dKurBNmpo+G8Zl4Sq/ztJ+2+DEsh0=
|
||||
modernc.org/cc/v4 v4.24.4/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
|
||||
modernc.org/ccgo/v4 v4.23.16 h1:Z2N+kk38b7SfySC1ZkpGLN2vthNJP1+ZzGZIlH7uBxo=
|
||||
modernc.org/ccgo/v4 v4.23.16/go.mod h1:nNma8goMTY7aQZQNTyN9AIoJfxav4nvTnvKThAeMDdo=
|
||||
modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE=
|
||||
modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ=
|
||||
modernc.org/gc/v2 v2.6.3 h1:aJVhcqAte49LF+mGveZ5KPlsp4tdGdAOT4sipJXADjw=
|
||||
modernc.org/gc/v2 v2.6.3/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
|
||||
modernc.org/libc v1.61.13 h1:3LRd6ZO1ezsFiX1y+bHd1ipyEHIJKvuprv0sLTBwLW8=
|
||||
modernc.org/libc v1.61.13/go.mod h1:8F/uJWL/3nNil0Lgt1Dpz+GgkApWh04N3el3hxJcA6E=
|
||||
modernc.org/cc/v4 v4.27.3 h1:uNCgn37E5U09mTv1XgskEVUJ8ADKpmFMPxzGJ0TSo+U=
|
||||
modernc.org/cc/v4 v4.27.3/go.mod h1:3YjcbCqhoTTHPycJDRl2WZKKFj0nwcOIPBfEZK0Hdk8=
|
||||
modernc.org/ccgo/v4 v4.32.4 h1:L5OB8rpEX4ZsXEQwGozRfJyJSFHbbNVOoQ59DU9/KuU=
|
||||
modernc.org/ccgo/v4 v4.32.4/go.mod h1:lY7f+fiTDHfcv6YlRgSkxYfhs+UvOEEzj49jAn2TOx0=
|
||||
modernc.org/fileutil v1.4.0 h1:j6ZzNTftVS054gi281TyLjHPp6CPHr2KCxEXjEbD6SM=
|
||||
modernc.org/fileutil v1.4.0/go.mod h1:EqdKFDxiByqxLk8ozOxObDSfcVOv/54xDs/DUHdvCUU=
|
||||
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
|
||||
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
|
||||
modernc.org/gc/v3 v3.1.2 h1:ZtDCnhonXSZexk/AYsegNRV1lJGgaNZJuKjJSWKyEqo=
|
||||
modernc.org/gc/v3 v3.1.2/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY=
|
||||
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
|
||||
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
|
||||
modernc.org/libc v1.72.0 h1:IEu559v9a0XWjw0DPoVKtXpO2qt5NVLAnFaBbjq+n8c=
|
||||
modernc.org/libc v1.72.0/go.mod h1:tTU8DL8A+XLVkEY3x5E/tO7s2Q/q42EtnNWda/L5QhQ=
|
||||
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
|
||||
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
|
||||
modernc.org/memory v1.8.2 h1:cL9L4bcoAObu4NkxOlKWBWtNHIsnnACGF/TbqQ6sbcI=
|
||||
modernc.org/memory v1.8.2/go.mod h1:ZbjSvMO5NQ1A2i3bWeDiVMxIorXwdClKE/0SZ+BMotU=
|
||||
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
|
||||
modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
|
||||
modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
|
||||
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
|
||||
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
|
||||
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
|
||||
modernc.org/sqlite v1.36.1 h1:bDa8BJUH4lg6EGkLbahKe/8QqoF8p9gArSc6fTqYhyQ=
|
||||
modernc.org/sqlite v1.36.1/go.mod h1:7MPwH7Z6bREicF9ZVUR78P1IKuxfZ8mRIDHD0iD+8TU=
|
||||
modernc.org/sqlite v1.50.0 h1:eMowQSWLK0MeiQTdmz3lqoF5dqclujdlIKeJA11+7oM=
|
||||
modernc.org/sqlite v1.50.0/go.mod h1:m0w8xhwYUVY3H6pSDwc3gkJ/irZT/0YEXwBlhaxQEew=
|
||||
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
|
||||
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
|
||||
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
||||
|
||||
22
db/hooks/api_tokens.go
Normal file
22
db/hooks/api_tokens.go
Normal file
@@ -0,0 +1,22 @@
|
||||
package hooks
|
||||
|
||||
import (
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
"github.com/pocketbase/pocketbase/tools/security"
|
||||
)
|
||||
|
||||
func CreateAPITokenHandler() func(e *core.RecordEvent) error {
|
||||
return func(e *core.RecordEvent) error {
|
||||
rawToken := "wanderer_key_" + security.RandomString(32)
|
||||
|
||||
hashedKey := security.SHA256(rawToken)
|
||||
|
||||
e.Record.Set("token", hashedKey)
|
||||
|
||||
// Temporarily store rawToken so we can display it once to the user
|
||||
e.Record.WithCustomData(true)
|
||||
e.Record.Set("rawToken", rawToken)
|
||||
|
||||
return e.Next()
|
||||
}
|
||||
}
|
||||
47
db/hooks/bootstrap.go
Normal file
47
db/hooks/bootstrap.go
Normal file
@@ -0,0 +1,47 @@
|
||||
package hooks
|
||||
|
||||
import (
|
||||
"cmp"
|
||||
"os"
|
||||
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
"github.com/spf13/cast"
|
||||
)
|
||||
|
||||
func OnBootstrapHandler() func(se *core.BootstrapEvent) error {
|
||||
return func(e *core.BootstrapEvent) error {
|
||||
if err := e.Next(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if e.App.Settings().Meta.AppName == "Acme" {
|
||||
e.App.Settings().Meta.AppName = "wanderer"
|
||||
}
|
||||
if v := os.Getenv("ORIGIN"); v != "" {
|
||||
e.App.Settings().Meta.AppURL = v
|
||||
}
|
||||
if v := cmp.Or(os.Getenv("POCKETBASE_SMTP_SENDER_ADDRESS"), os.Getenv("POCKETBASE_SMTP_SENDER_ADRESS")); v != "" {
|
||||
e.App.Settings().Meta.SenderAddress = v
|
||||
}
|
||||
if v := os.Getenv("POCKETBASE_SMTP_SENDER_NAME"); v != "" {
|
||||
e.App.Settings().Meta.SenderName = v
|
||||
}
|
||||
if v := os.Getenv("POCKETBASE_SMTP_ENABLED"); v != "" {
|
||||
e.App.Settings().SMTP.Enabled = cast.ToBool(v)
|
||||
}
|
||||
if v := os.Getenv("POCKETBASE_SMTP_HOST"); v != "" {
|
||||
e.App.Settings().SMTP.Host = v
|
||||
}
|
||||
if v := os.Getenv("POCKETBASE_SMTP_PORT"); v != "" {
|
||||
e.App.Settings().SMTP.Port = cast.ToInt(v)
|
||||
}
|
||||
if v := os.Getenv("POCKETBASE_SMTP_USERNAME"); v != "" {
|
||||
e.App.Settings().SMTP.Username = v
|
||||
}
|
||||
if v := os.Getenv("POCKETBASE_SMTP_PASSWORD"); v != "" {
|
||||
e.App.Settings().SMTP.Password = v
|
||||
}
|
||||
|
||||
return e.App.Save(e.App.Settings())
|
||||
}
|
||||
}
|
||||
65
db/hooks/comments.go
Normal file
65
db/hooks/comments.go
Normal file
@@ -0,0 +1,65 @@
|
||||
package hooks
|
||||
|
||||
import (
|
||||
"pocketbase/federation"
|
||||
"pocketbase/util"
|
||||
|
||||
pub "github.com/go-ap/activitypub"
|
||||
"github.com/meilisearch/meilisearch-go"
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
)
|
||||
|
||||
func CreateCommentHandler() func(e *core.RecordRequestEvent) error {
|
||||
return func(e *core.RecordRequestEvent) error {
|
||||
|
||||
e.Next()
|
||||
|
||||
userActor, err := e.App.FindFirstRecordByData("activitypub_actors", "user", e.Auth.Id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ctx, err := util.GetSafeActorContext(e.Request, userActor)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = federation.CreateCommentActivity(e.App, ctx, e.Record, pub.CreateType)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func UpdateCommentHandler() func(e *core.RecordRequestEvent) error {
|
||||
return func(e *core.RecordRequestEvent) error {
|
||||
userActor, err := e.App.FindFirstRecordByData("activitypub_actors", "user", e.Auth.Id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ctx, err := util.GetSafeActorContext(e.Request, userActor)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = federation.CreateCommentActivity(e.App, ctx, e.Record, pub.UpdateType)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return e.Next()
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func DeleteCommentHandler(client meilisearch.ServiceManager) func(e *core.RecordRequestEvent) error {
|
||||
return func(e *core.RecordRequestEvent) error {
|
||||
|
||||
err := federation.CreateCommentDeleteActivity(e.App, client, e.Record)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return e.Next()
|
||||
}
|
||||
}
|
||||
57
db/hooks/feed.go
Normal file
57
db/hooks/feed.go
Normal file
@@ -0,0 +1,57 @@
|
||||
package hooks
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"pocketbase/util"
|
||||
"strings"
|
||||
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
)
|
||||
|
||||
func ListFeedHandler() func(e *core.RecordsListRequestEvent) error {
|
||||
return func(e *core.RecordsListRequestEvent) error {
|
||||
|
||||
for _, r := range e.Records {
|
||||
var item *core.Record
|
||||
var err error
|
||||
|
||||
typ := r.GetString("type")
|
||||
typ = strings.Trim(typ, "\"")
|
||||
|
||||
itemId := r.GetString("item")
|
||||
itemId = strings.Trim(itemId, "\"")
|
||||
|
||||
switch typ {
|
||||
case string(util.TrailFeed):
|
||||
item, err = e.App.FindRecordById("trails", itemId)
|
||||
case string(util.ListFeed):
|
||||
item, err = e.App.FindRecordById("lists", itemId)
|
||||
case string(util.SummitLogFeed):
|
||||
item, err = e.App.FindRecordById("summit_logs", itemId)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if item == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
errs := e.App.ExpandRecord(item, []string{"author"}, nil)
|
||||
if len(errs) > 0 {
|
||||
return fmt.Errorf("failed to expand author: %v", errs)
|
||||
}
|
||||
|
||||
if typ == string(util.TrailFeed) {
|
||||
errs := e.App.ExpandRecord(item, []string{"category"}, nil)
|
||||
if len(errs) > 0 {
|
||||
return fmt.Errorf("failed to expand category: %v", errs)
|
||||
}
|
||||
}
|
||||
|
||||
r.MergeExpand(map[string]any{"item": item})
|
||||
}
|
||||
return e.Next()
|
||||
}
|
||||
}
|
||||
24
db/hooks/follow.go
Normal file
24
db/hooks/follow.go
Normal file
@@ -0,0 +1,24 @@
|
||||
package hooks
|
||||
|
||||
import (
|
||||
"pocketbase/federation"
|
||||
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
)
|
||||
|
||||
func CreateFollowHandler() func(e *core.RecordRequestEvent) error {
|
||||
return func(e *core.RecordRequestEvent) error {
|
||||
e.Next()
|
||||
federation.CreateFollowActivity(e.App, e.Record)
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func DeleteFollowHandler() func(e *core.RecordRequestEvent) error {
|
||||
return func(e *core.RecordRequestEvent) error {
|
||||
federation.CreateUnfollowActivity(e.App, e.Record)
|
||||
|
||||
return e.Next()
|
||||
}
|
||||
}
|
||||
146
db/hooks/integrations.go
Normal file
146
db/hooks/integrations.go
Normal file
@@ -0,0 +1,146 @@
|
||||
package hooks
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"pocketbase/util"
|
||||
|
||||
"github.com/pocketbase/pocketbase/apis"
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
"github.com/pocketbase/pocketbase/tools/security"
|
||||
)
|
||||
|
||||
func ListIntegrationHandler() func(e *core.RecordsListRequestEvent) error {
|
||||
return func(e *core.RecordsListRequestEvent) error {
|
||||
if e.HasSuperuserAuth() {
|
||||
return e.Next()
|
||||
}
|
||||
for _, r := range e.Records {
|
||||
|
||||
err := censorIntegrationSecrets(r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return e.Next()
|
||||
}
|
||||
}
|
||||
|
||||
func CreateIntegrationHandler() func(e *core.RecordEvent) error {
|
||||
return func(e *core.RecordEvent) error {
|
||||
err := encryptIntegrationSecrets(e.App, e.Record)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return e.Next()
|
||||
}
|
||||
}
|
||||
|
||||
func CreateUpdateIntegrationSuccessHandler() func(e *core.RecordEvent) error {
|
||||
return func(e *core.RecordEvent) error {
|
||||
err := censorIntegrationSecrets(e.Record)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return e.Next()
|
||||
}
|
||||
}
|
||||
|
||||
func UpdateIntegrationHandler() func(e *core.RecordEvent) error {
|
||||
return func(e *core.RecordEvent) error {
|
||||
err := encryptIntegrationSecrets(e.App, e.Record)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return e.Next()
|
||||
}
|
||||
}
|
||||
|
||||
func censorIntegrationSecrets(r *core.Record) error {
|
||||
secrets := map[string][]string{
|
||||
"strava": {"clientSecret", "refreshToken", "accessToken", "expiresAt"},
|
||||
"komoot": {"password"},
|
||||
"hammerhead": {"password"},
|
||||
}
|
||||
for key, secretKeys := range secrets {
|
||||
if integrationString := r.GetString(key); integrationString != "" {
|
||||
var integration map[string]interface{}
|
||||
if err := json.Unmarshal([]byte(integrationString), &integration); err != nil {
|
||||
return err
|
||||
}
|
||||
if integration == nil {
|
||||
continue
|
||||
}
|
||||
for _, secretKey := range secretKeys {
|
||||
integration[secretKey] = ""
|
||||
}
|
||||
b, err := json.Marshal(integration)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
r.Set(key, string(b))
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func encryptIntegrationSecrets(app core.App, r *core.Record) error {
|
||||
encryptionKey := os.Getenv("POCKETBASE_ENCRYPTION_KEY")
|
||||
if len(encryptionKey) == 0 {
|
||||
return apis.NewBadRequestError("POCKETBASE_ENCRYPTION_KEY not set", nil)
|
||||
}
|
||||
|
||||
secrets := map[string][]string{
|
||||
"strava": {"clientSecret", "refreshToken", "accessToken", "expiresAt"},
|
||||
"komoot": {"password"},
|
||||
"hammerhead": {"password"},
|
||||
}
|
||||
|
||||
original, _ := app.FindRecordById("integrations", r.Id)
|
||||
|
||||
for key, secretKeys := range secrets {
|
||||
if integrationString := r.GetString(key); integrationString != "" {
|
||||
var integration map[string]interface{}
|
||||
if err := json.Unmarshal([]byte(integrationString), &integration); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, secretKey := range secretKeys {
|
||||
// If the secret is already encrypted, we don't re-encrypt it.
|
||||
// TODO: This is a bit of a hack, we should handle this in a more robust way (e.g.
|
||||
// storing flag on the record or prefixing encrypted strings with enc: or smilar).
|
||||
// Doing that would also potentially allow us to support key rotation in the future.
|
||||
if secret, ok := integration[secretKey].(string); ok && len(secret) > 0 && !util.CanDecryptSecret(secret) {
|
||||
encryptedSecret, err := security.Encrypt([]byte(secret), encryptionKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
integration[secretKey] = encryptedSecret
|
||||
} else if original != nil {
|
||||
|
||||
originalString := original.GetString(key)
|
||||
var originalIntegration map[string]interface{}
|
||||
if err := json.Unmarshal([]byte(originalString), &originalIntegration); err != nil {
|
||||
return err
|
||||
}
|
||||
if integration == nil {
|
||||
continue
|
||||
}
|
||||
integration[secretKey] = originalIntegration[secretKey]
|
||||
}
|
||||
}
|
||||
|
||||
b, err := json.Marshal(integration)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
r.Set(key, string(b))
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
105
db/hooks/list.go
Normal file
105
db/hooks/list.go
Normal file
@@ -0,0 +1,105 @@
|
||||
package hooks
|
||||
|
||||
import (
|
||||
"pocketbase/federation"
|
||||
"pocketbase/util"
|
||||
|
||||
pub "github.com/go-ap/activitypub"
|
||||
"github.com/meilisearch/meilisearch-go"
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
)
|
||||
|
||||
func CreateListHandler(client meilisearch.ServiceManager) func(e *core.RecordEvent) error {
|
||||
return func(e *core.RecordEvent) error {
|
||||
record := e.Record
|
||||
|
||||
author, err := e.App.FindRecordById("activitypub_actors", record.GetString(("author")))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := util.IndexLists(e.App, []*core.Record{record}, client); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !author.GetBool("isLocal") {
|
||||
// this happens if someone fetches a remote list
|
||||
// we create a stub list record for later reference
|
||||
// no need to create an activity for that
|
||||
return e.Next()
|
||||
}
|
||||
|
||||
err = e.Next()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = federation.CreateListActivity(e.App, e.Record, pub.CreateType)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = util.InsertIntoFeed(e.App, author.Id, author.Id, record.Id, util.ListFeed)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func UpdateListHandler(client meilisearch.ServiceManager) func(e *core.RecordEvent) error {
|
||||
return func(e *core.RecordEvent) error {
|
||||
record := e.Record
|
||||
author, err := e.App.FindRecordById("activitypub_actors", record.GetString(("author")))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = util.UpdateList(e.App, record, author, client)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !author.GetBool("isLocal") {
|
||||
// this happens if someone fetches a remote list
|
||||
// we create a stub list record for later reference
|
||||
// no need to create an activity for that
|
||||
return e.Next()
|
||||
}
|
||||
|
||||
err = e.Next()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = federation.CreateListActivity(e.App, e.Record, pub.CreateType)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func DeleteListHandler(client meilisearch.ServiceManager) func(e *core.RecordEvent) error {
|
||||
return func(e *core.RecordEvent) error {
|
||||
record := e.Record
|
||||
_, err := client.Index("lists").DeleteDocument(record.Id, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = federation.CreateListDeleteActivity(e.App, record)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = util.DeleteFromFeed(e.App, record.Id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return e.Next()
|
||||
}
|
||||
}
|
||||
56
db/hooks/list_share.go
Normal file
56
db/hooks/list_share.go
Normal file
@@ -0,0 +1,56 @@
|
||||
package hooks
|
||||
|
||||
import (
|
||||
"pocketbase/federation"
|
||||
"pocketbase/util"
|
||||
|
||||
"github.com/meilisearch/meilisearch-go"
|
||||
"github.com/pocketbase/dbx"
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
)
|
||||
|
||||
func CreateListShareHandler(client meilisearch.ServiceManager) func(e *core.RecordRequestEvent) error {
|
||||
return func(e *core.RecordRequestEvent) error {
|
||||
err := e.Next()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
record := e.Record
|
||||
listId := record.GetString("list")
|
||||
shares, err := e.App.FindAllRecords("list_share",
|
||||
dbx.NewExp("list = {:listId}", dbx.Params{"listId": listId}),
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
actorIds := make([]string, len(shares))
|
||||
for i, r := range shares {
|
||||
actorIds[i] = r.GetString("actor")
|
||||
}
|
||||
err = util.UpdateListShares(listId, actorIds, client)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = federation.CreateAnnounceActivity(e.App, record, federation.ListAnnounceType)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func DeleteListShareHandler(client meilisearch.ServiceManager) func(e *core.RecordRequestEvent) error {
|
||||
return func(e *core.RecordRequestEvent) error {
|
||||
record := e.Record
|
||||
listId := record.GetString("list")
|
||||
err := util.UpdateListShares(listId, []string{}, client)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return e.Next()
|
||||
}
|
||||
}
|
||||
96
db/hooks/summit_logs.go
Normal file
96
db/hooks/summit_logs.go
Normal file
@@ -0,0 +1,96 @@
|
||||
package hooks
|
||||
|
||||
import (
|
||||
"pocketbase/federation"
|
||||
"pocketbase/util"
|
||||
|
||||
pub "github.com/go-ap/activitypub"
|
||||
"github.com/meilisearch/meilisearch-go"
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
)
|
||||
|
||||
func CreateSummitLogHandler(client meilisearch.ServiceManager) func(e *core.RecordRequestEvent) error {
|
||||
return func(e *core.RecordRequestEvent) error {
|
||||
|
||||
err := e.Next()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
userActor, err := e.App.FindFirstRecordByData("activitypub_actors", "user", e.Auth.Id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ctx, err := util.GetSafeActorContext(e.Request, userActor)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
trail, err := e.App.FindRecordById("trails", e.Record.GetString("trail"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := util.IndexTrails(e.App, []*core.Record{trail}, client); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = federation.CreateSummitLogActivity(e.App, ctx, e.Record, pub.CreateType)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func UpdateSummitLogHandler() func(e *core.RecordRequestEvent) error {
|
||||
return func(e *core.RecordRequestEvent) error {
|
||||
|
||||
err := e.Next()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
userActor, err := e.App.FindFirstRecordByData("activitypub_actors", "user", e.Auth.Id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ctx, err := util.GetSafeActorContext(e.Request, userActor)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = federation.CreateSummitLogActivity(e.App, ctx, e.Record, pub.UpdateType)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func DeleteSummitLogHandler(client meilisearch.ServiceManager) func(e *core.RecordRequestEvent) error {
|
||||
return func(e *core.RecordRequestEvent) error {
|
||||
err := e.Next()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
trail, err := e.App.FindRecordById("trails", e.Record.GetString("trail"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := util.IndexTrails(e.App, []*core.Record{trail}, client); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = federation.CreateSummitLogDeleteActivity(e.App, e.Record)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
118
db/hooks/trail_like.go
Normal file
118
db/hooks/trail_like.go
Normal file
@@ -0,0 +1,118 @@
|
||||
package hooks
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"pocketbase/federation"
|
||||
"pocketbase/util"
|
||||
|
||||
"github.com/meilisearch/meilisearch-go"
|
||||
"github.com/pocketbase/dbx"
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
)
|
||||
|
||||
func CreateTrailLikeHandler(client meilisearch.ServiceManager) func(e *core.RecordEvent) error {
|
||||
return func(e *core.RecordEvent) error {
|
||||
err := e.Next()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
record := e.Record
|
||||
|
||||
trailId := record.GetString("trail")
|
||||
actorId := record.GetString("actor")
|
||||
actor, err := e.App.FindRecordById("activitypub_actors", actorId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
trail, err := e.App.FindRecordById("trails", trailId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
likes, err := e.App.FindAllRecords("trail_like",
|
||||
dbx.NewExp("trail = {:trailId}", dbx.Params{"trailId": trailId}),
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
trail.Set("like_count", len(likes))
|
||||
err = e.App.UnsafeWithoutHooks().Save(trail)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
actorIds := make([]string, len(likes))
|
||||
for i, r := range likes {
|
||||
actorIds[i] = r.GetString("actor")
|
||||
}
|
||||
err = util.UpdateTrailLikes(trailId, actorIds, client)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !actor.GetBool("isLocal") {
|
||||
// this happens if someone likes a remote trail
|
||||
// we create a local copy
|
||||
// no need to create an activity for that
|
||||
return nil
|
||||
}
|
||||
|
||||
err = federation.CreateLikeActivity(e.App, record)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func DeleteTrailLikeHandler(client meilisearch.ServiceManager) func(e *core.RecordEvent) error {
|
||||
return func(e *core.RecordEvent) error {
|
||||
|
||||
record := e.Record
|
||||
|
||||
trailId := record.GetString("trail")
|
||||
actorId := record.GetString("actor")
|
||||
actor, err := e.App.FindRecordById("activitypub_actors", actorId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// trail might deleted be already if this is called as part of a cascade
|
||||
trail, err := e.App.FindRecordById("trails", trailId)
|
||||
if err != nil && err == sql.ErrNoRows {
|
||||
return nil
|
||||
} else if err != nil {
|
||||
return err
|
||||
}
|
||||
likes, err := e.App.CountRecords("trail_like", dbx.NewExp("trail={:trail}", dbx.Params{"trail": trailId}))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
trail.Set("like_count", likes)
|
||||
err = e.App.UnsafeWithoutHooks().Save(trail)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = util.UpdateTrailLikes(trailId, []string{}, client)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !actor.GetBool("isLocal") {
|
||||
// this happens if someone likes a remote trail
|
||||
// we create a local copy
|
||||
// no need to create an activity for that
|
||||
return nil
|
||||
}
|
||||
|
||||
err = federation.CreateUnlikeActivity(e.App, record)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return e.Next()
|
||||
}
|
||||
}
|
||||
57
db/hooks/trail_share.go
Normal file
57
db/hooks/trail_share.go
Normal file
@@ -0,0 +1,57 @@
|
||||
package hooks
|
||||
|
||||
import (
|
||||
"pocketbase/federation"
|
||||
"pocketbase/util"
|
||||
|
||||
"github.com/meilisearch/meilisearch-go"
|
||||
"github.com/pocketbase/dbx"
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
)
|
||||
|
||||
func CreateTrailShareHandler(client meilisearch.ServiceManager) func(e *core.RecordRequestEvent) error {
|
||||
return func(e *core.RecordRequestEvent) error {
|
||||
err := e.Next()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
record := e.Record
|
||||
|
||||
trailId := record.GetString("trail")
|
||||
shares, err := e.App.FindAllRecords("trail_share",
|
||||
dbx.NewExp("trail = {:trailId}", dbx.Params{"trailId": trailId}),
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
actorIds := make([]string, len(shares))
|
||||
for i, r := range shares {
|
||||
actorIds[i] = r.GetString("actor")
|
||||
}
|
||||
err = util.UpdateTrailShares(trailId, actorIds, client)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = federation.CreateAnnounceActivity(e.App, record, federation.TrailAnnounceType)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func DeleteTrailShareHandler(client meilisearch.ServiceManager) func(e *core.RecordRequestEvent) error {
|
||||
return func(e *core.RecordRequestEvent) error {
|
||||
record := e.Record
|
||||
|
||||
trailId := record.GetString("trail")
|
||||
err := util.UpdateTrailShares(trailId, []string{}, client)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return e.Next()
|
||||
}
|
||||
}
|
||||
122
db/hooks/trails.go
Normal file
122
db/hooks/trails.go
Normal file
@@ -0,0 +1,122 @@
|
||||
package hooks
|
||||
|
||||
import (
|
||||
"log"
|
||||
"pocketbase/federation"
|
||||
"pocketbase/util"
|
||||
"time"
|
||||
|
||||
"github.com/go-ap/activitypub"
|
||||
pub "github.com/go-ap/activitypub"
|
||||
"github.com/meilisearch/meilisearch-go"
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
)
|
||||
|
||||
func CreateTrailHandler(client meilisearch.ServiceManager) func(e *core.RecordEvent) error {
|
||||
return func(e *core.RecordEvent) error {
|
||||
record := e.Record
|
||||
|
||||
userActor, err := e.App.FindRecordById("activitypub_actors", record.GetString(("author")))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := util.IndexTrails(e.App, []*core.Record{record}, client); err != nil {
|
||||
return err
|
||||
}
|
||||
if !userActor.GetBool("isLocal") {
|
||||
// this happens if someone fetches a remote trail
|
||||
// we create a stub trail record for later reference
|
||||
// no need to create an activity for that
|
||||
return e.Next()
|
||||
}
|
||||
|
||||
err = e.Next()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ctx, err := util.GetSafeActorContext(nil, userActor)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = federation.CreateTrailActivity(e.App, ctx, e.Record, activitypub.CreateType)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = util.InsertIntoFeed(e.App, userActor.Id, userActor.Id, record.Id, util.TrailFeed)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func UpdateTrailHandler(client meilisearch.ServiceManager) func(e *core.RecordEvent) error {
|
||||
return func(e *core.RecordEvent) error {
|
||||
record := e.Record
|
||||
userActor, err := e.App.FindRecordById("activitypub_actors", record.GetString(("author")))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = util.UpdateTrail(e.App, record, userActor, client)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !userActor.GetBool("isLocal") {
|
||||
// this happens if someone fetches a remote trail
|
||||
// we create a stub trail record for later reference
|
||||
// no need to create an activity for that
|
||||
return e.Next()
|
||||
}
|
||||
|
||||
err = e.Next()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ctx, err := util.GetSafeActorContext(nil, userActor)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = federation.CreateTrailActivity(e.App, ctx, e.Record, pub.UpdateType)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func DeleteTrailHandler(client meilisearch.ServiceManager) func(e *core.RecordEvent) error {
|
||||
return func(e *core.RecordEvent) error {
|
||||
record := e.Record
|
||||
task, err := client.Index("trails").DeleteDocument(record.Id, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
interval := 500 * time.Millisecond
|
||||
_, err = client.WaitForTask(task.TaskUID, interval)
|
||||
if err != nil {
|
||||
log.Fatalf("Error waiting for task completion: %v", err)
|
||||
}
|
||||
|
||||
err = federation.CreateTrailDeleteActivity(e.App, e.Record)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = util.DeleteFromFeed(e.App, record.Id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return e.Next()
|
||||
}
|
||||
}
|
||||
101
db/hooks/users.go
Normal file
101
db/hooks/users.go
Normal file
@@ -0,0 +1,101 @@
|
||||
package hooks
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"pocketbase/util"
|
||||
|
||||
"github.com/meilisearch/meilisearch-go"
|
||||
"github.com/pocketbase/dbx"
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
)
|
||||
|
||||
func CreateUserHandler(client meilisearch.ServiceManager) func(e *core.RecordEvent) error {
|
||||
return func(e *core.RecordEvent) error {
|
||||
userId := e.Record.Id
|
||||
|
||||
err := createDefaultUserSettings(e.App, e.Record.Id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
actor, err := util.ActorFromUser(e.App, e.Record)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
searchRules := map[string]interface{}{
|
||||
"lists": map[string]string{
|
||||
"filter": "public = true OR author = " + actor.Id + " OR shares = " + userId,
|
||||
},
|
||||
"trails": map[string]string{
|
||||
"filter": "public = true OR author = " + actor.Id + " OR shares = " + userId,
|
||||
},
|
||||
}
|
||||
|
||||
token, err := util.GenerateMeilisearchToken(searchRules, client)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
e.Record.Set("token", token)
|
||||
if err := e.App.Save(e.Record); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return e.Next()
|
||||
}
|
||||
}
|
||||
|
||||
func UpdateUserHandler(client meilisearch.ServiceManager) func(e *core.RecordEvent) error {
|
||||
return func(e *core.RecordEvent) error {
|
||||
actor, err := e.App.FindFirstRecordByData("activitypub_actors", "user", e.Record.Id)
|
||||
if err != nil {
|
||||
return e.Next()
|
||||
}
|
||||
|
||||
icon := ""
|
||||
origin := os.Getenv("ORIGIN")
|
||||
if origin != "" && e.Record.GetString("avatar") != "" {
|
||||
icon = fmt.Sprintf("%s/api/v1/files/_pb_users_auth_/%s/%s", origin, e.Record.Id, e.Record.GetString("avatar"))
|
||||
}
|
||||
actor.Set("icon", icon)
|
||||
if err := e.App.Save(actor); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
trails, err := e.App.FindRecordsByFilter("trails", "author={:author}", "", -1, 0, dbx.Params{"author": actor.Id})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(trails) > 0 {
|
||||
if err := util.IndexTrails(e.App, trails, client); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
lists, err := e.App.FindRecordsByFilter("lists", "author={:author}", "", -1, 0, dbx.Params{"author": actor.Id})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(lists) > 0 {
|
||||
if err := util.IndexLists(e.App, lists, client); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return e.Next()
|
||||
}
|
||||
}
|
||||
|
||||
func createDefaultUserSettings(app core.App, userId string) error {
|
||||
collection, err := app.FindCollectionByNameOrId("settings")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
settings := core.NewRecord(collection)
|
||||
settings.Set("language", "en")
|
||||
settings.Set("unit", "metric")
|
||||
settings.Set("mapFocus", "trails")
|
||||
settings.Set("user", userId)
|
||||
return app.Save(settings)
|
||||
}
|
||||
927
db/integrations/hammerhead/hammerhead.go
Normal file
927
db/integrations/hammerhead/hammerhead.go
Normal file
@@ -0,0 +1,927 @@
|
||||
package hammerhead
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
|
||||
"math"
|
||||
"os"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/meilisearch/meilisearch-go"
|
||||
"github.com/pocketbase/dbx"
|
||||
"github.com/pocketbase/pocketbase/apis"
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
"github.com/pocketbase/pocketbase/tools/filesystem"
|
||||
"github.com/pocketbase/pocketbase/tools/security"
|
||||
"github.com/tkrajina/gpxgo/gpx"
|
||||
|
||||
"pocketbase/services/trailmerge"
|
||||
"pocketbase/util"
|
||||
)
|
||||
|
||||
func SyncHammerhead(app core.App, client meilisearch.ServiceManager) error {
|
||||
integrations, err := app.FindAllRecords("integrations", dbx.NewExp("true"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, i := range integrations {
|
||||
encryptionKey := os.Getenv("POCKETBASE_ENCRYPTION_KEY")
|
||||
if len(encryptionKey) == 0 {
|
||||
return errors.New("POCKETBASE_ENCRYPTION_KEY not set")
|
||||
}
|
||||
|
||||
userId := i.GetString("user")
|
||||
actor, err := app.FindFirstRecordByData("activitypub_actors", "user", userId)
|
||||
if err != nil {
|
||||
warning := fmt.Sprintf("no actor found for user: %s\n", userId)
|
||||
fmt.Print(warning)
|
||||
app.Logger().Warn(warning)
|
||||
continue
|
||||
}
|
||||
|
||||
ctx, err := util.GetSafeActorContext(nil, actor)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
hammerheadString := i.GetString("hammerhead")
|
||||
hammerheadIntegration := HammerheadIntegration{
|
||||
Planned: true,
|
||||
Completed: true,
|
||||
Merge: trailmerge.DefaultIntegrationAutoMergeSettings(),
|
||||
}
|
||||
json.Unmarshal([]byte(hammerheadString), &hammerheadIntegration)
|
||||
|
||||
if !hammerheadIntegration.Active || hammerheadIntegration.Email == "" || hammerheadIntegration.Password == "" {
|
||||
continue
|
||||
}
|
||||
h := &HammerheadApi{}
|
||||
|
||||
decryptedPassword, err := security.Decrypt(hammerheadIntegration.Password, encryptionKey)
|
||||
if err != nil {
|
||||
warning := fmt.Sprintf("unable to decrypt password: %v\n", err)
|
||||
fmt.Print(warning)
|
||||
app.Logger().Warn(warning)
|
||||
continue
|
||||
}
|
||||
|
||||
err = h.Login(hammerheadIntegration.Email, string(decryptedPassword))
|
||||
if err != nil {
|
||||
warning := fmt.Sprintf("Hammerhead login failed: %v\n", err)
|
||||
fmt.Print(warning)
|
||||
app.Logger().Warn(warning)
|
||||
continue
|
||||
}
|
||||
|
||||
page := 0
|
||||
totalPages := 0
|
||||
stopped := false
|
||||
|
||||
var after int64 = 0
|
||||
if hammerheadIntegration.After != "" {
|
||||
t, err := time.Parse("2006-01-02", hammerheadIntegration.After)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
t = t.UTC()
|
||||
|
||||
after = t.Unix()
|
||||
}
|
||||
|
||||
if hammerheadIntegration.Planned {
|
||||
page = 0
|
||||
totalPages = 0
|
||||
stopped = false
|
||||
|
||||
for page <= totalPages && !stopped {
|
||||
curTotalPages := totalPages
|
||||
tours, curTotalPages, err := h.fetchTours(page)
|
||||
if err != nil {
|
||||
warning := fmt.Sprintf("error fetching tours from Hammerhead: %v\n", err)
|
||||
fmt.Print(warning)
|
||||
app.Logger().Warn(warning)
|
||||
break
|
||||
}
|
||||
|
||||
if curTotalPages > totalPages {
|
||||
totalPages = curTotalPages
|
||||
}
|
||||
|
||||
err, stopped = syncTrailWithTours(app, client, ctx, h, actor, hammerheadIntegration, tours, after)
|
||||
if err != nil {
|
||||
warning := fmt.Sprintf("error syncing Hammerhead tours with trails: %v\n", err)
|
||||
fmt.Print(warning)
|
||||
app.Logger().Warn(warning)
|
||||
break
|
||||
}
|
||||
|
||||
page += 1
|
||||
}
|
||||
}
|
||||
|
||||
if hammerheadIntegration.Completed {
|
||||
page = 0
|
||||
totalPages = 0
|
||||
stopped = false
|
||||
|
||||
for page <= totalPages && !stopped {
|
||||
curTotalPages := totalPages
|
||||
tours, curTotalPages, err := h.fetchActivities(page)
|
||||
if err != nil {
|
||||
warning := fmt.Sprintf("error fetching tours from Hammerhead: %v\n", err)
|
||||
fmt.Print(warning)
|
||||
app.Logger().Warn(warning)
|
||||
break
|
||||
}
|
||||
|
||||
if curTotalPages > totalPages {
|
||||
totalPages = curTotalPages
|
||||
}
|
||||
|
||||
err, stopped = syncTrailWithActivities(app, client, ctx, h, actor, hammerheadIntegration, tours, after)
|
||||
if err != nil {
|
||||
warning := fmt.Sprintf("error syncing Hammerhead tours with trails: %v\n", err)
|
||||
fmt.Print(warning)
|
||||
app.Logger().Warn(warning)
|
||||
break
|
||||
}
|
||||
|
||||
page += 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type BasicAuthToken struct {
|
||||
Key string
|
||||
Value string
|
||||
}
|
||||
|
||||
func (b BasicAuthToken) Apply(req *http.Request) {
|
||||
req.Header.Set("Authorization", "Bearer "+b.Value)
|
||||
}
|
||||
|
||||
type HammerheadApi struct {
|
||||
UserID string
|
||||
Token string
|
||||
}
|
||||
|
||||
func (h *HammerheadApi) buildHeader() *BasicAuthToken {
|
||||
if h.UserID != "" && h.Token != "" {
|
||||
return &BasicAuthToken{h.UserID, h.Token}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func getToken(uri string, auth *BasicAuthToken) ([]byte, error) {
|
||||
client := &http.Client{}
|
||||
|
||||
var jsonStr = []byte(`{"grant_type": "password", "username": "` + auth.Key + `", "password": "` + auth.Value + `"}`)
|
||||
|
||||
req, err := http.NewRequest("POST", uri, bytes.NewBuffer(jsonStr))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
return nil, fmt.Errorf("error retrieving auth token from Hammerhead (%d): %s", resp.StatusCode, string(body))
|
||||
}
|
||||
|
||||
return io.ReadAll(resp.Body)
|
||||
}
|
||||
|
||||
func (h *HammerheadApi) UploadActivities(e *core.RequestEvent) error {
|
||||
files, err := e.FindUploadedFiles("file")
|
||||
if err != nil {
|
||||
if errors.Is(err, http.ErrMissingFile) {
|
||||
return apis.NewBadRequestError("file field is required", err)
|
||||
}
|
||||
return apis.NewBadRequestError("invalid multipart payload", err)
|
||||
}
|
||||
|
||||
if len(files) == 0 {
|
||||
return apis.NewBadRequestError("file field is required", nil)
|
||||
}
|
||||
|
||||
fileToUpload := files[0]
|
||||
reader, err := fileToUpload.Reader.Open()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer reader.Close()
|
||||
|
||||
var buf bytes.Buffer
|
||||
writer := multipart.NewWriter(&buf)
|
||||
|
||||
part, err := writer.CreateFormFile("file", fileToUpload.OriginalName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := io.Copy(part, reader); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
contentType := writer.FormDataContentType()
|
||||
|
||||
if err := writer.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
currentURI := fmt.Sprintf("https://dashboard.hammerhead.io/v1/users/%s/routes/import/file", h.UserID)
|
||||
|
||||
if _, err := sendPostRequest(currentURI, &buf, contentType, h.buildHeader()); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func sendPostRequest(url string, body io.Reader, contentType string, auth *BasicAuthToken) ([]byte, error) {
|
||||
client := &http.Client{}
|
||||
req, err := http.NewRequest("POST", url, body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if contentType != "" {
|
||||
req.Header.Set("Content-Type", contentType)
|
||||
}
|
||||
|
||||
if auth != nil {
|
||||
auth.Apply(req)
|
||||
}
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
return nil, fmt.Errorf("error sending request to Hammerhead (%d): %s", resp.StatusCode, string(body))
|
||||
}
|
||||
|
||||
return io.ReadAll(resp.Body)
|
||||
}
|
||||
|
||||
func sendGetRequest(url string, auth *BasicAuthToken) ([]byte, error) {
|
||||
client := &http.Client{}
|
||||
req, err := http.NewRequest("GET", url, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if auth != nil {
|
||||
auth.Apply(req)
|
||||
}
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
return nil, fmt.Errorf("error sending request to Hammerhead (%d): %s", resp.StatusCode, string(body))
|
||||
}
|
||||
|
||||
return io.ReadAll(resp.Body)
|
||||
}
|
||||
|
||||
func (h *HammerheadApi) Login(email, password string) error {
|
||||
url := "https://dashboard.hammerhead.io/v1/auth/token"
|
||||
|
||||
body, err := getToken(url, &BasicAuthToken{email, password})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var data LoginResponse
|
||||
json.Unmarshal(body, &data)
|
||||
|
||||
h.Token = data.Token
|
||||
derivedUserID, err := extractUserIDFromToken(data.Token)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to determine Hammerhead user id automatically: %w", err)
|
||||
}
|
||||
h.UserID = derivedUserID
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func extractUserIDFromToken(token string) (string, error) {
|
||||
parts := strings.Split(token, ".")
|
||||
if len(parts) < 2 {
|
||||
return "", errors.New("token is not a JWT")
|
||||
}
|
||||
|
||||
payload, err := base64.RawURLEncoding.DecodeString(parts[1])
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("unable to decode JWT payload: %w", err)
|
||||
}
|
||||
|
||||
var claims map[string]any
|
||||
if err := json.Unmarshal(payload, &claims); err != nil {
|
||||
return "", fmt.Errorf("unable to decode JWT claims: %w", err)
|
||||
}
|
||||
|
||||
if value, ok := claims["sub"].(string); ok && value != "" {
|
||||
return value, nil
|
||||
}
|
||||
|
||||
return "", errors.New("no sub claim found in token")
|
||||
}
|
||||
|
||||
func (h *HammerheadApi) fetchActivities(page int) ([]HammerheadActivityResponse, int, error) {
|
||||
|
||||
currentUri := fmt.Sprintf("https://dashboard.hammerhead.io/v1/users/%s/activities?perPage=50&page=%d&search=&orderBy=NEWEST&ascending=true", h.UserID, page)
|
||||
|
||||
body, err := sendGetRequest(currentUri, h.buildHeader())
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
var data HammerheadActivitiesResponse
|
||||
json.Unmarshal(body, &data)
|
||||
|
||||
tours := data.Tours
|
||||
|
||||
return tours, data.Pages, nil
|
||||
}
|
||||
|
||||
func (h *HammerheadApi) fetchTours(page int) ([]HammerheadTourResponse, int, error) {
|
||||
|
||||
currentUri := fmt.Sprintf("https://dashboard.hammerhead.io/v1/users/%s/routes?perPage=50&page=%d&search=&orderBy=NEWEST&ascending=true&exclude=archive", h.UserID, page)
|
||||
body, err := sendGetRequest(currentUri, h.buildHeader())
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
var data HammerheadToursResponse
|
||||
json.Unmarshal(body, &data)
|
||||
|
||||
tours := data.Data
|
||||
|
||||
return tours, data.TotalPages, nil
|
||||
}
|
||||
|
||||
func (h *HammerheadApi) fetchDetailedActivity(tour HammerheadActivityResponse) (*HammerheadActivity, error) {
|
||||
|
||||
url := fmt.Sprintf("https://dashboard.hammerhead.io/v1/users/%s/activities/%s/details", h.UserID, tour.ID)
|
||||
body, err := sendGetRequest(url, h.buildHeader())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var data *HammerheadActivity
|
||||
json.Unmarshal(body, &data)
|
||||
return data, nil
|
||||
}
|
||||
|
||||
func (h *HammerheadApi) fetchDetailedTour(tour HammerheadTourResponse) (*HammerheadTour, error) {
|
||||
|
||||
url := fmt.Sprintf("https://dashboard.hammerhead.io/v1/users/%s/routes/%s", h.UserID, tour.ID)
|
||||
body, err := sendGetRequest(url, h.buildHeader())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var data *HammerheadTour
|
||||
json.Unmarshal(body, &data)
|
||||
return data, nil
|
||||
}
|
||||
|
||||
func syncTrailWithTours(app core.App, client meilisearch.ServiceManager, ctx context.Context, k *HammerheadApi, actor *core.Record, integration HammerheadIntegration, tours []HammerheadTourResponse, after int64) (error, bool) {
|
||||
for _, tour := range tours {
|
||||
existingTrail, err := util.FindTrailByExternalReference(app, "hammerhead", tour.ID)
|
||||
if err != nil {
|
||||
return err, true
|
||||
}
|
||||
if existingTrail != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
detailedTour, err := k.fetchDetailedTour(tour)
|
||||
if err != nil {
|
||||
app.Logger().Warn(fmt.Sprintf("Unable to fetch details for tour '%s': %v", tour.Name, err))
|
||||
continue
|
||||
}
|
||||
|
||||
if detailedTour.CreatedAt.Unix() < after {
|
||||
return nil, true
|
||||
}
|
||||
|
||||
if detailedTour.Distance <= 0 {
|
||||
app.Logger().Warn(fmt.Sprintf("Skipping Hammerhead tour '%s' with zero distance", tour.Name))
|
||||
continue
|
||||
}
|
||||
|
||||
gpx, err := generateTourGPX(detailedTour)
|
||||
if err != nil {
|
||||
app.Logger().Warn(fmt.Sprintf("Unable to generate GPX for tour '%s': %v", tour.Name, err))
|
||||
continue
|
||||
}
|
||||
|
||||
trailID, err := createTrailFromTour(app, detailedTour, gpx, actor.Id)
|
||||
if err != nil {
|
||||
app.Logger().Warn(fmt.Sprintf("Unable to create trail for tour '%s': %v", tour.Name, err))
|
||||
continue
|
||||
}
|
||||
if err := trailmerge.TryAutoMergeImportedTrail(app, client, ctx, actor, trailID, integration.Merge); err != nil {
|
||||
app.Logger().Warn(fmt.Sprintf("Unable to auto-merge imported Hammerhead tour '%s': %v", tour.Name, err))
|
||||
}
|
||||
}
|
||||
|
||||
return nil, false
|
||||
}
|
||||
|
||||
func syncTrailWithActivities(app core.App, client meilisearch.ServiceManager, ctx context.Context, k *HammerheadApi, actor *core.Record, integration HammerheadIntegration, tours []HammerheadActivityResponse, after int64) (error, bool) {
|
||||
for _, tour := range tours {
|
||||
existingTrail, err := util.FindTrailByExternalReference(app, "hammerhead", tour.ID)
|
||||
if err != nil {
|
||||
return err, true
|
||||
}
|
||||
if existingTrail != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
detailedTour, err := k.fetchDetailedActivity(tour)
|
||||
if err != nil {
|
||||
app.Logger().Warn(fmt.Sprintf("Unable to fetch details for tour '%s': %v", tour.Name, err))
|
||||
continue
|
||||
}
|
||||
|
||||
if detailedTour.ActivityData.CreatedAt.Unix() < after {
|
||||
return nil, true
|
||||
}
|
||||
|
||||
distance, ok := activityDistance(detailedTour)
|
||||
if !ok || distance <= 0 {
|
||||
app.Logger().Warn(fmt.Sprintf("Skipping Hammerhead activity '%s' with zero distance", tour.Name))
|
||||
continue
|
||||
}
|
||||
|
||||
gpx, err := generateActivityGPX(detailedTour)
|
||||
if err != nil {
|
||||
app.Logger().Warn(fmt.Sprintf("Unable to generate GPX for tour '%s': %v", tour.Name, err))
|
||||
continue
|
||||
}
|
||||
|
||||
trailID, err := createTrailFromActivity(app, detailedTour, gpx, actor.Id)
|
||||
if err != nil {
|
||||
app.Logger().Warn(fmt.Sprintf("Unable to create trail for tour '%s': %v", tour.Name, err))
|
||||
continue
|
||||
}
|
||||
if err := trailmerge.TryAutoMergeImportedTrail(app, client, ctx, actor, trailID, integration.Merge); err != nil {
|
||||
app.Logger().Warn(fmt.Sprintf("Unable to auto-merge imported Hammerhead activity '%s': %v", tour.Name, err))
|
||||
}
|
||||
}
|
||||
|
||||
return nil, false
|
||||
}
|
||||
|
||||
func activityDistance(detailedTour *HammerheadActivity) (float64, bool) {
|
||||
idDistance := slices.IndexFunc(detailedTour.ActivityData.ActivityInfo, func(c HammerheadInfo) bool { return c.Key == "TYPE_DISTANCE_ID" })
|
||||
if idDistance < 0 {
|
||||
return 0, false
|
||||
}
|
||||
|
||||
return detailedTour.ActivityData.ActivityInfo[idDistance].Value.Value, true
|
||||
}
|
||||
|
||||
func createTrailFromActivity(app core.App, detailedTour *HammerheadActivity, gpx *filesystem.File, actor string) (string, error) {
|
||||
trailid := security.RandomStringWithAlphabet(core.DefaultIdLength, core.DefaultIdAlphabet)
|
||||
|
||||
collection, err := app.FindCollectionByNameOrId("trails")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
record := core.NewRecord(collection)
|
||||
|
||||
category, _ := app.FindFirstRecordByData("categories", "name", "Biking" /*ToDo: Mapping*/)
|
||||
categoryId := ""
|
||||
if category != nil {
|
||||
categoryId = category.Id
|
||||
}
|
||||
|
||||
diffculty := "easy" // ToDo: calculate difficulty
|
||||
|
||||
idDistance := slices.IndexFunc(detailedTour.ActivityData.ActivityInfo, func(c HammerheadInfo) bool { return c.Key == "TYPE_DISTANCE_ID" })
|
||||
idElevationGain := slices.IndexFunc(detailedTour.ActivityData.ActivityInfo, func(c HammerheadInfo) bool { return c.Key == "TYPE_ELEVATION_GAIN_ID" })
|
||||
idElevationLoss := slices.IndexFunc(detailedTour.ActivityData.ActivityInfo, func(c HammerheadInfo) bool { return c.Key == "TYPE_ELEVATION_LOSS_ID" })
|
||||
|
||||
duration := 0
|
||||
for _, lap := range detailedTour.ActivityData.Laps {
|
||||
duration += lap.ActiveTime
|
||||
}
|
||||
|
||||
startLat := float64(0)
|
||||
startLng := float64(0)
|
||||
for i, lat := range detailedTour.RecordData.Lat {
|
||||
if lat != float64(0) {
|
||||
startLat = lat
|
||||
startLng = detailedTour.RecordData.Lng[i]
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
record.Load(map[string]any{
|
||||
"id": trailid,
|
||||
"name": detailedTour.ActivityData.Name,
|
||||
"public": false,
|
||||
"completed": true,
|
||||
"distance": detailedTour.ActivityData.ActivityInfo[idDistance].Value.Value,
|
||||
"elevation_gain": detailedTour.ActivityData.ActivityInfo[idElevationGain].Value.Value,
|
||||
"elevation_loss": detailedTour.ActivityData.ActivityInfo[idElevationLoss].Value.Value,
|
||||
"duration": duration / 1000,
|
||||
"date": detailedTour.ActivityData.CreatedAt,
|
||||
"external_provider": "hammerhead",
|
||||
"external_id": detailedTour.ActivityData.ID,
|
||||
"lat": startLat,
|
||||
"lon": startLng,
|
||||
"difficulty": diffculty,
|
||||
"category": categoryId,
|
||||
"author": actor,
|
||||
})
|
||||
|
||||
if gpx != nil {
|
||||
record.Set("gpx", gpx)
|
||||
}
|
||||
|
||||
if err := app.Save(record); err != nil {
|
||||
return "", err
|
||||
}
|
||||
if err := util.EnsureTrailExternalReference(app, trailid, "hammerhead", detailedTour.ActivityData.ID); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
collection, err = app.FindCollectionByNameOrId("summit_logs")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
summitLogRecord := core.NewRecord(collection)
|
||||
summitLogRecord.Load(map[string]any{
|
||||
"distance": detailedTour.ActivityData.ActivityInfo[idDistance].Value.Value,
|
||||
"elevation_gain": detailedTour.ActivityData.ActivityInfo[idElevationGain].Value.Value,
|
||||
"elevation_loss": detailedTour.ActivityData.ActivityInfo[idElevationLoss].Value.Value,
|
||||
"duration": duration / 1000,
|
||||
"date": detailedTour.ActivityData.CreatedAt,
|
||||
"author": actor,
|
||||
"trail": trailid,
|
||||
})
|
||||
if err := app.Save(summitLogRecord); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return trailid, nil
|
||||
}
|
||||
|
||||
func createTrailFromTour(app core.App, detailedTour *HammerheadTour, gpx *filesystem.File, actor string) (string, error) {
|
||||
trailid := security.RandomStringWithAlphabet(core.DefaultIdLength, core.DefaultIdAlphabet)
|
||||
|
||||
collection, err := app.FindCollectionByNameOrId("trails")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
record := core.NewRecord(collection)
|
||||
|
||||
category, _ := app.FindFirstRecordByData("categories", "name", "Biking" /*ToDo: Mapping*/)
|
||||
categoryId := ""
|
||||
if category != nil {
|
||||
categoryId = category.Id
|
||||
}
|
||||
|
||||
diffculty := "easy" // ToDo: calculate difficulty
|
||||
|
||||
record.Load(map[string]any{
|
||||
"id": trailid,
|
||||
"name": detailedTour.Name,
|
||||
"public": detailedTour.IsPublic,
|
||||
"distance": detailedTour.Distance,
|
||||
"elevation_gain": detailedTour.Elevation.Gain,
|
||||
"elevation_loss": detailedTour.Elevation.Loss,
|
||||
"date": detailedTour.CreatedAt,
|
||||
"lat": detailedTour.StartLocation.Lat,
|
||||
"lon": detailedTour.StartLocation.Lng,
|
||||
"difficulty": diffculty,
|
||||
"category": categoryId,
|
||||
"author": actor,
|
||||
})
|
||||
|
||||
if gpx != nil {
|
||||
record.Set("gpx", gpx)
|
||||
}
|
||||
|
||||
if err := app.Save(record); err != nil {
|
||||
return "", err
|
||||
}
|
||||
if err := util.EnsureTrailExternalReference(app, trailid, "hammerhead", detailedTour.ID); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return trailid, nil
|
||||
}
|
||||
|
||||
func generateActivityGPX(detailedTour *HammerheadActivity) (*filesystem.File, error) {
|
||||
times := len(detailedTour.RecordData.Timestamp)
|
||||
if times == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
var points []gpx.GPXPoint
|
||||
const zeroEps = 1e-4
|
||||
|
||||
// iterate over timestamps and only add points when lat/lng exist for the same index
|
||||
for i := 0; i < times; i++ {
|
||||
// ensure we have latitude and longitude for this index
|
||||
if i < len(detailedTour.RecordData.Lat) && i < len(detailedTour.RecordData.Lng) {
|
||||
lat := detailedTour.RecordData.Lat[i]
|
||||
lng := detailedTour.RecordData.Lng[i]
|
||||
|
||||
// exclude near (0,0) garbage points
|
||||
if math.Abs(lat) < zeroEps && math.Abs(lng) < zeroEps {
|
||||
continue
|
||||
}
|
||||
|
||||
t := detailedTour.RecordData.Timestamp[i]
|
||||
|
||||
elevation := float64(0)
|
||||
if i < len(detailedTour.RecordData.Elevation) {
|
||||
elevation = detailedTour.RecordData.Elevation[i] / 1000.0
|
||||
}
|
||||
|
||||
points = append(points, gpx.GPXPoint{
|
||||
Point: gpx.Point{
|
||||
Latitude: lat,
|
||||
Longitude: lng,
|
||||
Elevation: *gpx.NewNullableFloat64(elevation),
|
||||
},
|
||||
Timestamp: time.Unix(int64(t), 0),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if len(points) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
gpxData := &gpx.GPX{
|
||||
Version: "1.1",
|
||||
Creator: "Hammerhead GPX Exporter",
|
||||
Tracks: []gpx.GPXTrack{
|
||||
{
|
||||
Name: detailedTour.ActivityData.Name,
|
||||
Segments: []gpx.GPXTrackSegment{
|
||||
{
|
||||
Points: points,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
gpxAsXML, err := gpxData.ToXml(gpx.ToXmlParams{Version: "1.1", Indent: true})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
gpxFile, err := filesystem.NewFileFromBytes(gpxAsXML, detailedTour.ActivityData.Name+".gpx")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return gpxFile, nil
|
||||
}
|
||||
|
||||
func generateTourGPX(detailedTour *HammerheadTour) (*filesystem.File, error) {
|
||||
|
||||
poly := detailedTour.RoutePolyline
|
||||
coords, err := decodePolyline(poly)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("decode polyline: %w", err)
|
||||
}
|
||||
if len(coords) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// try to get elevation polyline (adjust field path if your struct differs)
|
||||
elevations := []float64{}
|
||||
// precision 100 is common for Valhalla elevation encodings; change if needed
|
||||
if decoded, err := decodeElevations(detailedTour.Elevation.Polyline, 100000); err == nil {
|
||||
elevations = decoded
|
||||
}
|
||||
|
||||
// Heuristic: detect if coords are (lng,lat) instead of (lat,lng).
|
||||
// Count how many points look valid in each orientation and pick the best.
|
||||
validAsLat := 0
|
||||
validAsLng := 0
|
||||
for _, c := range coords {
|
||||
// treat c[0] as lat, c[1] as lng
|
||||
if c[0] >= -90 && c[0] <= 90 && c[1] >= -180 && c[1] <= 180 {
|
||||
validAsLat++
|
||||
}
|
||||
// treat c[1] as lat, c[0] as lng (swapped)
|
||||
if c[1] >= -90 && c[1] <= 90 && c[0] >= -180 && c[0] <= 180 {
|
||||
validAsLng++
|
||||
}
|
||||
}
|
||||
swap := false
|
||||
if validAsLng > validAsLat {
|
||||
swap = true
|
||||
}
|
||||
|
||||
var points []gpx.GPXPoint
|
||||
for i, c := range coords {
|
||||
lat := c[0]
|
||||
lng := c[1]
|
||||
if swap {
|
||||
lat, lng = c[1], c[0]
|
||||
}
|
||||
|
||||
// choose elevation:
|
||||
elevation := 0.0
|
||||
if len(elevations) == len(coords) {
|
||||
elevation = elevations[i]
|
||||
} else if len(elevations) > 0 {
|
||||
// map index proportionally if lengths differ
|
||||
j := int(math.Round(float64(i) * float64(len(elevations)-1) / float64(len(coords)-1)))
|
||||
if j < 0 {
|
||||
j = 0
|
||||
}
|
||||
if j >= len(elevations) {
|
||||
j = len(elevations) - 1
|
||||
}
|
||||
elevation = elevations[j]
|
||||
}
|
||||
|
||||
points = append(points, gpx.GPXPoint{
|
||||
Point: gpx.Point{
|
||||
Latitude: lat,
|
||||
Longitude: lng,
|
||||
Elevation: *gpx.NewNullableFloat64(elevation),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
gpxData := &gpx.GPX{
|
||||
Version: "1.1",
|
||||
Creator: "Hammerhead GPX Exporter",
|
||||
Tracks: []gpx.GPXTrack{
|
||||
{
|
||||
Name: detailedTour.Name,
|
||||
Segments: []gpx.GPXTrackSegment{
|
||||
{
|
||||
Points: points,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
gpxAsXML, err := gpxData.ToXml(gpx.ToXmlParams{Version: "1.1", Indent: true})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
gpxFile, err := filesystem.NewFileFromBytes(gpxAsXML, detailedTour.Name+".gpx")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return gpxFile, nil
|
||||
}
|
||||
|
||||
// decodePolyline decodes an encoded polyline string (Google Polyline Algorithm)
|
||||
// returns slice of [lat, lng] pairs (precision 1e5).
|
||||
func decodePolyline(s string) ([][2]float64, error) {
|
||||
if s == "" {
|
||||
return nil, nil
|
||||
}
|
||||
var coords [][2]float64
|
||||
index := 0
|
||||
lat := 0
|
||||
lng := 0
|
||||
for index < len(s) {
|
||||
// decode latitude
|
||||
result := 0
|
||||
shift := uint(0)
|
||||
for {
|
||||
if index >= len(s) {
|
||||
return nil, fmt.Errorf("invalid polyline encoding")
|
||||
}
|
||||
b := int(s[index]) - 63
|
||||
index++
|
||||
result |= (b & 0x1F) << shift
|
||||
shift += 5
|
||||
if b < 0x20 {
|
||||
break
|
||||
}
|
||||
}
|
||||
dlat := (result >> 1) ^ (-(result & 1))
|
||||
lat += dlat
|
||||
|
||||
// decode longitude
|
||||
result = 0
|
||||
shift = 0
|
||||
for {
|
||||
if index >= len(s) {
|
||||
return nil, fmt.Errorf("invalid polyline encoding")
|
||||
}
|
||||
b := int(s[index]) - 63
|
||||
index++
|
||||
result |= (b & 0x1F) << shift
|
||||
shift += 5
|
||||
if b < 0x20 {
|
||||
break
|
||||
}
|
||||
}
|
||||
dlng := (result >> 1) ^ (-(result & 1))
|
||||
lng += dlng
|
||||
|
||||
coords = append(coords, [2]float64{float64(lat) / 1e5, float64(lng) / 1e5})
|
||||
}
|
||||
|
||||
// Auto-normalize scale if values are out of realistic lat/lon ranges.
|
||||
// Some providers use different precision/scales; repeatedly divide by 10
|
||||
// until all values fit into valid ranges.
|
||||
if len(coords) > 0 {
|
||||
maxLat := 0.0
|
||||
maxLng := 0.0
|
||||
for _, c := range coords {
|
||||
if abs := math.Abs(c[0]); abs > maxLat {
|
||||
maxLat = abs
|
||||
}
|
||||
if abs := math.Abs(c[1]); abs > maxLng {
|
||||
maxLng = abs
|
||||
}
|
||||
}
|
||||
// If values are too large (e.g. > 90 lat or > 180 lon), rescale down.
|
||||
for (maxLat > 90.0 || maxLng > 180.0) && (maxLat > 0 && maxLng > 0) {
|
||||
for i := range coords {
|
||||
coords[i][0] /= 10.0
|
||||
coords[i][1] /= 10.0
|
||||
}
|
||||
maxLat /= 10.0
|
||||
maxLng /= 10.0
|
||||
}
|
||||
}
|
||||
|
||||
return coords, nil
|
||||
}
|
||||
|
||||
// decodeElevations decodes a single-dimension delta-encoded polyline string.
|
||||
// precision is the divisor (e.g. 100 for centi-meters -> meters). Returns elevation values in same units as precision (meters if precision=100).
|
||||
func decodeElevations(s string, precision float64) ([]float64, error) {
|
||||
if s == "" {
|
||||
return nil, nil
|
||||
}
|
||||
var elevs []float64
|
||||
index := 0
|
||||
val := 0
|
||||
for index < len(s) {
|
||||
result := 0
|
||||
shift := uint(0)
|
||||
for {
|
||||
if index >= len(s) {
|
||||
return nil, fmt.Errorf("invalid elevation encoding")
|
||||
}
|
||||
b := int(s[index]) - 63
|
||||
index++
|
||||
result |= (b & 0x1F) << shift
|
||||
shift += 5
|
||||
if b < 0x20 {
|
||||
break
|
||||
}
|
||||
}
|
||||
d := (result >> 1) ^ (-(result & 1))
|
||||
val += d
|
||||
elevs = append(elevs, float64(val)/precision)
|
||||
}
|
||||
return elevs, nil
|
||||
}
|
||||
209
db/integrations/hammerhead/models.go
Normal file
209
db/integrations/hammerhead/models.go
Normal file
@@ -0,0 +1,209 @@
|
||||
package hammerhead
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"pocketbase/services/trailmerge"
|
||||
)
|
||||
|
||||
type HammerheadToursResponse struct {
|
||||
TotalItems int `json:"totalItems"`
|
||||
TotalPages int `json:"totalPages"`
|
||||
PerPage int `json:"perPage"`
|
||||
CurrentPage int `json:"currentPage"`
|
||||
Data []HammerheadTourResponse `json:"data"`
|
||||
}
|
||||
type HammerheadTourResponse struct {
|
||||
StartLocationName string `json:"startLocationName"`
|
||||
IsAutoImported bool `json:"isAutoImported"`
|
||||
SummaryPolyline string `json:"summaryPolyline"`
|
||||
IsStarred bool `json:"isStarred"`
|
||||
IsPublic bool `json:"isPublic"`
|
||||
Collections any `json:"collections"`
|
||||
Gain int `json:"gain"`
|
||||
Distance float64 `json:"distance"`
|
||||
Name string `json:"name"`
|
||||
RoutingType string `json:"routingType"`
|
||||
ID string `json:"id"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
UpdatedAt time.Time `json:"updatedAt"`
|
||||
Source string `json:"source"`
|
||||
}
|
||||
|
||||
type HammerheadTourElevation struct {
|
||||
Gain float64 `json:"gain"`
|
||||
Loss float64 `json:"loss"`
|
||||
Min float64 `json:"min"`
|
||||
Max float64 `json:"max"`
|
||||
Source string `json:"source"`
|
||||
Polyline string `json:"polyline"`
|
||||
}
|
||||
type HammerheadLocation struct {
|
||||
Lat float64 `json:"lat"`
|
||||
Lng float64 `json:"lng"`
|
||||
}
|
||||
type HammerheadWaypoint struct {
|
||||
Lat float64 `json:"lat"`
|
||||
Lng float64 `json:"lng"`
|
||||
WaypointType string `json:"waypointType"`
|
||||
PolylineIndex int `json:"polylineIndex"`
|
||||
}
|
||||
|
||||
type HammerheadTour struct {
|
||||
ID string `json:"id"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
Name string `json:"name"`
|
||||
Distance float64 `json:"distance"`
|
||||
Elevation HammerheadTourElevation `json:"elevation"`
|
||||
IsStarred bool `json:"isStarred"`
|
||||
StartLocationName string `json:"startLocationName"`
|
||||
EndLocationName string `json:"endLocationName"`
|
||||
StartLocation HammerheadLocation `json:"startLocation"`
|
||||
EndLocation HammerheadLocation `json:"endLocation"`
|
||||
Waypoints []HammerheadWaypoint `json:"waypoints"`
|
||||
Collections []string `json:"collections"`
|
||||
RoutePolyline string `json:"routePolyline"`
|
||||
SummaryPolyline string `json:"summaryPolyline"`
|
||||
Source string `json:"source"`
|
||||
SourceID string `json:"sourceId"`
|
||||
IsPublic bool `json:"isPublic"`
|
||||
ImageVersion string `json:"imageVersion"`
|
||||
IsAutoImported bool `json:"isAutoImported"`
|
||||
UpdatedAt time.Time `json:"updatedAt"`
|
||||
Bounds []HammerheadLocation `json:"bounds"`
|
||||
}
|
||||
|
||||
type HammerheadIntegration struct {
|
||||
Active bool `json:"active"`
|
||||
Email string `json:"email"`
|
||||
Password string `json:"password"`
|
||||
Planned bool `json:"planned"`
|
||||
Completed bool `json:"completed"`
|
||||
After string `json:"after,omitempty"`
|
||||
Merge trailmerge.IntegrationAutoMergeSettings `json:"merge"`
|
||||
}
|
||||
|
||||
type LoginResponse struct {
|
||||
Token string `json:"access_token"`
|
||||
Type string `json:"token_type"`
|
||||
Expires int `json:"expires_in"`
|
||||
}
|
||||
|
||||
type HammerheadActivitiesResponse struct {
|
||||
Items int `json:"totalItems"`
|
||||
Pages int `json:"totalPages"`
|
||||
PerPage int `json:"perPage"`
|
||||
Tours []HammerheadActivityResponse `json:"data"`
|
||||
}
|
||||
|
||||
type HammerheadActivityResponse struct {
|
||||
ID string `json:"id"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
Name string `json:"name"`
|
||||
Client string `json:"client"`
|
||||
ActiveTime int `json:"activeTime"`
|
||||
Duration HammerheadTourDuration `json:"duration"`
|
||||
Sync HammerheadSync `json:"partners"`
|
||||
ActivityInfo []HammerheadInfo `json:"activityInfo"`
|
||||
}
|
||||
type HammerheadInfoValue struct {
|
||||
Format string `json:"format"`
|
||||
Value float64 `json:"value"`
|
||||
}
|
||||
type HammerheadInfo struct {
|
||||
Key string `json:"key"`
|
||||
Value HammerheadInfoValue `json:"value"`
|
||||
}
|
||||
type HammerheadPartner struct {
|
||||
Partner string `json:"partner"`
|
||||
NeedsUpload bool `json:"needsUpload"`
|
||||
ExternalID string `json:"externalId"`
|
||||
Attempts int `json:"attempts"`
|
||||
UploadedAt time.Time `json:"uploadedAt"`
|
||||
}
|
||||
type HammerheadSync struct {
|
||||
Description string `json:"description"`
|
||||
Tags []any `json:"tags"`
|
||||
Synced bool `json:"synced"`
|
||||
Partners []HammerheadPartner `json:"partners"`
|
||||
}
|
||||
type HammerheadTourDuration struct {
|
||||
ElapsedTime int `json:"elapsedTime"`
|
||||
StartTime time.Time `json:"startTime"`
|
||||
EndTime time.Time `json:"endTime"`
|
||||
}
|
||||
|
||||
type HammerheadActivity struct {
|
||||
ActivityData HammerheadActivityData `json:"activityData"`
|
||||
SessionData HammerheadSessionData `json:"sessionData"`
|
||||
RecordData HammerheadRecordData `json:"recordData"`
|
||||
ShiftData HammerheadShiftData `json:"shiftData"`
|
||||
LapData HammerheadLapData `json:"lapData"`
|
||||
DeviceBatteryData HammerheadDeviceBatteryData `json:"deviceBatteryData"`
|
||||
}
|
||||
type HammerheadDuration struct {
|
||||
ElapsedTime int `json:"elapsedTime"`
|
||||
StartTime time.Time `json:"startTime"`
|
||||
EndTime time.Time `json:"endTime"`
|
||||
}
|
||||
type HammerheadLapDetail struct {
|
||||
ActiveTime int `json:"activeTime"`
|
||||
Duration HammerheadDuration `json:"duration"`
|
||||
LapNumber int `json:"lapNumber"`
|
||||
Pauses []HammerheadDuration `json:"pauses"`
|
||||
LapInfo []HammerheadInfo `json:"lapInfo"`
|
||||
Trigger string `json:"trigger"`
|
||||
}
|
||||
type HammerheadActivityData struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
BikeID string `json:"bikeId"`
|
||||
Client string `json:"client"`
|
||||
ActiveTime int `json:"activeTime"`
|
||||
Duration HammerheadDuration `json:"duration"`
|
||||
ActivityInfo []HammerheadInfo `json:"activityInfo"`
|
||||
Laps []HammerheadLapDetail `json:"laps"`
|
||||
Polyline string `json:"polyline"`
|
||||
Sync HammerheadSync `json:"sync"`
|
||||
ActivityType string `json:"activityType"`
|
||||
Climbs []HammerheadClimb `json:"climbs"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
UpdatedAt time.Time `json:"updatedAt"`
|
||||
}
|
||||
type HammerheadClimb struct {
|
||||
StartDistance float64 `json:"startDistance"`
|
||||
EndDistance float64 `json:"endDistance"`
|
||||
Distance float64 `json:"distance"`
|
||||
}
|
||||
type HammerheadSessionData struct {
|
||||
ThresholdPower int `json:"thresholdPower"`
|
||||
FrontGears []int `json:"frontGears"`
|
||||
RearGears []int `json:"rearGears"`
|
||||
}
|
||||
type HammerheadRecordData struct {
|
||||
Distance []float64 `json:"distance"`
|
||||
Timestamp []int `json:"timestamp"`
|
||||
Elevation []float64 `json:"elevation"`
|
||||
Grade []float64 `json:"grade"`
|
||||
Lat []float64 `json:"lat"`
|
||||
Lng []float64 `json:"lng"`
|
||||
Speed []float64 `json:"speed"`
|
||||
Power []any `json:"power"`
|
||||
Temperature []int `json:"temperature"`
|
||||
}
|
||||
type HammerheadShiftData struct {
|
||||
Timestamp []int `json:"timestamp"`
|
||||
FrontChange []bool `json:"frontChange"`
|
||||
FrontGear []int `json:"frontGear"`
|
||||
RearGear []int `json:"rearGear"`
|
||||
FrontGearNum []int `json:"frontGearNum"`
|
||||
RearGearNum []int `json:"rearGearNum"`
|
||||
}
|
||||
type HammerheadLapData struct {
|
||||
Timestamp []int `json:"timestamp"`
|
||||
Trigger []string `json:"trigger"`
|
||||
}
|
||||
type HammerheadDeviceBatteryData struct {
|
||||
Timestamp []int `json:"timestamp"`
|
||||
DeviceBattery []int `json:"deviceBattery"`
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
package komoot
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
@@ -13,14 +13,18 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/meilisearch/meilisearch-go"
|
||||
"github.com/pocketbase/dbx"
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
"github.com/pocketbase/pocketbase/tools/filesystem"
|
||||
"github.com/pocketbase/pocketbase/tools/security"
|
||||
"github.com/twpayne/go-gpx"
|
||||
"github.com/tkrajina/gpxgo/gpx"
|
||||
|
||||
"pocketbase/services/trailmerge"
|
||||
"pocketbase/util"
|
||||
)
|
||||
|
||||
func SyncKomoot(app core.App) error {
|
||||
func SyncKomoot(app core.App, client meilisearch.ServiceManager) error {
|
||||
integrations, err := app.FindAllRecords("integrations", dbx.NewExp("true"))
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -40,12 +44,17 @@ func SyncKomoot(app core.App) error {
|
||||
app.Logger().Warn(warning)
|
||||
continue
|
||||
}
|
||||
actorId := actor.Id
|
||||
|
||||
ctx, err := util.GetSafeActorContext(nil, actor)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
komootString := i.GetString("komoot")
|
||||
komootIntegration := KomootIntegration{
|
||||
Planned: true,
|
||||
Completed: true,
|
||||
Merge: trailmerge.DefaultIntegrationAutoMergeSettings(),
|
||||
}
|
||||
json.Unmarshal([]byte(komootString), &komootIntegration)
|
||||
|
||||
@@ -69,25 +78,27 @@ func SyncKomoot(app core.App) error {
|
||||
app.Logger().Warn(warning)
|
||||
continue
|
||||
}
|
||||
hasNewTours := true
|
||||
page := 0
|
||||
for hasNewTours {
|
||||
tours, err := k.fetchTours(page)
|
||||
totalPages := 1
|
||||
for page := 0; page < totalPages; page++ {
|
||||
tours, tp, err := k.fetchTours(page)
|
||||
if err != nil {
|
||||
warning := fmt.Sprintf("error fetching tours from komoot: %v\n", err)
|
||||
warning := fmt.Sprintf("error fetching tours from komoot (page %d): %v\n", page, err)
|
||||
fmt.Print(warning)
|
||||
app.Logger().Warn(warning)
|
||||
continue
|
||||
break
|
||||
}
|
||||
totalPages = tp
|
||||
|
||||
hasNewTours, err = syncTrailWithTours(app, k, komootIntegration, userId, actorId, tours)
|
||||
allAlreadySynced, err := syncTrailWithTours(app, client, ctx, k, komootIntegration, userId, actor, tours)
|
||||
if err != nil {
|
||||
warning := fmt.Sprintf("error syncing komoot tours with trails: %v\n", err)
|
||||
fmt.Print(warning)
|
||||
app.Logger().Warn(warning)
|
||||
continue
|
||||
break
|
||||
}
|
||||
if allAlreadySynced {
|
||||
break
|
||||
}
|
||||
page += 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -157,20 +168,18 @@ func (k *KomootApi) Login(email, password string) error {
|
||||
|
||||
return nil
|
||||
}
|
||||
func (k *KomootApi) fetchTours(page int) ([]KomootTour, error) {
|
||||
func (k *KomootApi) fetchTours(page int) ([]KomootTour, int, error) {
|
||||
currentUri := fmt.Sprintf("https://api.komoot.de/v007/users/%s/tours/?page=%d&sort_field=date&sort_direction=desc&limit=30", k.UserID, page)
|
||||
|
||||
body, err := sendRequest(currentUri, k.buildHeader())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
var data KomootToursResponse
|
||||
json.Unmarshal(body, &data)
|
||||
|
||||
tours := data.Embedded.Tours
|
||||
|
||||
return tours, nil
|
||||
return data.Embedded.Tours, data.Page.TotalPages, nil
|
||||
}
|
||||
|
||||
func (k *KomootApi) fetchDetailedTour(tour KomootTour) (*DetailedKomootTour, error) {
|
||||
@@ -185,17 +194,25 @@ func (k *KomootApi) fetchDetailedTour(tour KomootTour) (*DetailedKomootTour, err
|
||||
return data, nil
|
||||
}
|
||||
|
||||
func syncTrailWithTours(app core.App, k *KomootApi, i KomootIntegration, user string, actor string, tours []KomootTour) (bool, error) {
|
||||
hasNewTours := false
|
||||
// syncTrailWithTours imports tours not yet in the DB. Returns allAlreadySynced=true
|
||||
// when every tour on this page was already imported, so the caller can stop paginating
|
||||
// early during incremental syncs. Tours skipped due to type filters do NOT count as
|
||||
// synced - only tours already present in the DB do.
|
||||
func syncTrailWithTours(app core.App, client meilisearch.ServiceManager, ctx context.Context, k *KomootApi, i KomootIntegration, user string, actor *core.Record, tours []KomootTour) (bool, error) {
|
||||
allAlreadySynced := true
|
||||
for _, tour := range tours {
|
||||
trails, err := app.FindRecordsByFilter("trails", "external_id = {:id}", "", 1, 0, dbx.Params{"id": strconv.Itoa(int(tour.ID))})
|
||||
existingTrail, err := util.FindTrailByExternalReference(app, "komoot", strconv.Itoa(int(tour.ID)))
|
||||
if err != nil {
|
||||
return hasNewTours, err
|
||||
return false, err
|
||||
}
|
||||
if len(trails) != 0 || (tour.Type == "tour_planned" && !i.Planned) || (tour.Type == "tour_recorded" && !i.Completed) {
|
||||
if existingTrail != nil {
|
||||
continue
|
||||
}
|
||||
// Tour is not yet in the DB - we must keep paginating regardless of type filter
|
||||
allAlreadySynced = false
|
||||
if (tour.Type == "tour_planned" && !i.Planned) || (tour.Type == "tour_recorded" && !i.Completed) {
|
||||
continue
|
||||
}
|
||||
hasNewTours = true
|
||||
detailedTour, err := k.fetchDetailedTour(tour)
|
||||
if err != nil {
|
||||
app.Logger().Warn(fmt.Sprintf("Unable to fetch details for tour '%s': %v", tour.Name, err))
|
||||
@@ -206,27 +223,30 @@ func syncTrailWithTours(app core.App, k *KomootApi, i KomootIntegration, user st
|
||||
app.Logger().Warn(fmt.Sprintf("Unable to generate GPX for tour '%s': %v", tour.Name, err))
|
||||
continue
|
||||
}
|
||||
wpIds, err := createWaypointsFromTour(app, detailedTour, user)
|
||||
if err != nil {
|
||||
app.Logger().Warn(fmt.Sprintf("Unable to create waypoints for tour '%s': %v", tour.Name, err))
|
||||
continue
|
||||
}
|
||||
err = createTrailFromTour(app, k, detailedTour, gpx, actor, wpIds)
|
||||
trailid, err := createTrailFromTour(app, k, detailedTour, gpx, user, actor.Id, i.Privacy)
|
||||
if err != nil {
|
||||
app.Logger().Warn(fmt.Sprintf("Unable to create trail for tour '%s': %v", tour.Name, err))
|
||||
continue
|
||||
}
|
||||
err = createWaypointsFromTour(app, detailedTour, user, trailid)
|
||||
if err != nil {
|
||||
app.Logger().Warn(fmt.Sprintf("Unable to create waypoints for tour '%s': %v", tour.Name, err))
|
||||
continue
|
||||
}
|
||||
if err := trailmerge.TryAutoMergeImportedTrail(app, client, ctx, actor, trailid, i.Merge); err != nil {
|
||||
app.Logger().Warn(fmt.Sprintf("Unable to auto-merge imported komoot tour '%s': %v", tour.Name, err))
|
||||
}
|
||||
|
||||
}
|
||||
return hasNewTours, nil
|
||||
return allAlreadySynced, nil
|
||||
}
|
||||
|
||||
func createTrailFromTour(app core.App, k *KomootApi, detailedTour *DetailedKomootTour, gpx *filesystem.File, actor string, wpIds []string) error {
|
||||
func createTrailFromTour(app core.App, k *KomootApi, detailedTour *DetailedKomootTour, gpx *filesystem.File, user string, actor string, privacy string) (string, error) {
|
||||
trailid := security.RandomStringWithAlphabet(core.DefaultIdLength, core.DefaultIdAlphabet)
|
||||
|
||||
collection, err := app.FindCollectionByNameOrId("trails")
|
||||
if err != nil {
|
||||
return err
|
||||
return "", err
|
||||
}
|
||||
|
||||
record := core.NewRecord(collection)
|
||||
@@ -252,12 +272,12 @@ func createTrailFromTour(app core.App, k *KomootApi, detailedTour *DetailedKomoo
|
||||
if len(detailedTour.Embedded.CoverImages.Embedded.Items) > 0 {
|
||||
photos, err = fetchRoutePhotos(k, detailedTour)
|
||||
if err != nil {
|
||||
return err
|
||||
return "", err
|
||||
}
|
||||
} else {
|
||||
photo, err := fetchPhoto(detailedTour.MapImage.Src, "", "")
|
||||
if err != nil {
|
||||
return err
|
||||
return "", err
|
||||
}
|
||||
photos = append(photos, photo)
|
||||
}
|
||||
@@ -267,10 +287,25 @@ func createTrailFromTour(app core.App, k *KomootApi, detailedTour *DetailedKomoo
|
||||
diffculty = "easy"
|
||||
}
|
||||
|
||||
public := detailedTour.Status == "public"
|
||||
if privacy == "settings" {
|
||||
privacySettings := struct {
|
||||
Trails string `json:"trails"`
|
||||
}{}
|
||||
|
||||
settings, _ := app.FindFirstRecordByData("settings", "user", user)
|
||||
err = settings.UnmarshalJSONField("privacy", &privacySettings)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
public = privacySettings.Trails == "public"
|
||||
}
|
||||
|
||||
record.Load(map[string]any{
|
||||
"id": trailid,
|
||||
"name": detailedTour.Name,
|
||||
"public": detailedTour.Status == "public",
|
||||
"public": public,
|
||||
"completed": detailedTour.Type == "tour_recorded",
|
||||
"distance": detailedTour.Distance,
|
||||
"elevation_gain": detailedTour.ElevationUp,
|
||||
"elevation_loss": detailedTour.ElevationDown,
|
||||
@@ -282,7 +317,6 @@ func createTrailFromTour(app core.App, k *KomootApi, detailedTour *DetailedKomoo
|
||||
"lon": detailedTour.StartPoint.Lng,
|
||||
"difficulty": diffculty,
|
||||
"category": categoryId,
|
||||
"waypoints": wpIds,
|
||||
"author": actor,
|
||||
})
|
||||
|
||||
@@ -294,13 +328,16 @@ func createTrailFromTour(app core.App, k *KomootApi, detailedTour *DetailedKomoo
|
||||
}
|
||||
|
||||
if err := app.Save(record); err != nil {
|
||||
return err
|
||||
return "", err
|
||||
}
|
||||
if err := util.EnsureTrailExternalReference(app, trailid, "komoot", strconv.Itoa(detailedTour.ID)); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if detailedTour.Type == "tour_recorded" {
|
||||
collection, err := app.FindCollectionByNameOrId("summit_logs")
|
||||
if err != nil {
|
||||
return err
|
||||
return "", err
|
||||
}
|
||||
|
||||
summitLogRecord := core.NewRecord(collection)
|
||||
@@ -314,25 +351,23 @@ func createTrailFromTour(app core.App, k *KomootApi, detailedTour *DetailedKomoo
|
||||
"trail": trailid,
|
||||
})
|
||||
if err := app.Save(summitLogRecord); err != nil {
|
||||
return err
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
return trailid, nil
|
||||
}
|
||||
|
||||
func createWaypointsFromTour(app core.App, tour *DetailedKomootTour, user string) ([]string, error) {
|
||||
func createWaypointsFromTour(app core.App, tour *DetailedKomootTour, user string, trailid string) error {
|
||||
collection, err := app.FindCollectionByNameOrId("waypoints")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return err
|
||||
}
|
||||
|
||||
wpIds := make([]string, len(tour.Embedded.Timeline.Embedded.Items))
|
||||
|
||||
for i, wp := range tour.Embedded.Timeline.Embedded.Items {
|
||||
for _, wp := range tour.Embedded.Timeline.Embedded.Items {
|
||||
photos, err := fetchWaypointPhotos(wp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return err
|
||||
}
|
||||
record := core.NewRecord(collection)
|
||||
|
||||
@@ -359,6 +394,7 @@ func createWaypointsFromTour(app core.App, tour *DetailedKomootTour, user string
|
||||
"icon": "circle",
|
||||
"author": user,
|
||||
"distance_from_start": 0,
|
||||
"trail": trailid,
|
||||
})
|
||||
|
||||
if photos != nil {
|
||||
@@ -366,13 +402,11 @@ func createWaypointsFromTour(app core.App, tour *DetailedKomootTour, user string
|
||||
}
|
||||
|
||||
if err := app.Save(record); err != nil {
|
||||
return nil, err
|
||||
return err
|
||||
}
|
||||
|
||||
wpIds[i] = record.Id
|
||||
}
|
||||
|
||||
return wpIds, nil
|
||||
return nil
|
||||
}
|
||||
|
||||
func fetchRoutePhotos(k *KomootApi, tour *DetailedKomootTour) ([]*filesystem.File, error) {
|
||||
@@ -388,9 +422,9 @@ func fetchRoutePhotos(k *KomootApi, tour *DetailedKomootTour) ([]*filesystem.Fil
|
||||
return nil, err
|
||||
}
|
||||
|
||||
photos := make([]*filesystem.File, data.Page.TotalElements)
|
||||
photos := make([]*filesystem.File, 0, len(data.Embedded.Items))
|
||||
|
||||
for i, img := range data.Embedded.Items {
|
||||
for _, img := range data.Embedded.Items {
|
||||
photo, err := fetchPhoto(img.Src, "", "")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -398,7 +432,7 @@ func fetchRoutePhotos(k *KomootApi, tour *DetailedKomootTour) ([]*filesystem.Fil
|
||||
if strings.HasSuffix(photo.Name, ".gif") {
|
||||
continue
|
||||
}
|
||||
photos[i] = photo
|
||||
photos = append(photos, photo)
|
||||
|
||||
//TODO: komoot photos can have location data. Maybe we should create a waypoint for those photos?
|
||||
}
|
||||
@@ -408,9 +442,9 @@ func fetchRoutePhotos(k *KomootApi, tour *DetailedKomootTour) ([]*filesystem.Fil
|
||||
|
||||
func fetchWaypointPhotos(wp Item) ([]*filesystem.File, error) {
|
||||
|
||||
photos := make([]*filesystem.File, len(wp.Embedded.Reference.Embedded.Images.Embedded.Items))
|
||||
photos := make([]*filesystem.File, 0, len(wp.Embedded.Reference.Embedded.Images.Embedded.Items))
|
||||
|
||||
for i, img := range wp.Embedded.Reference.Embedded.Images.Embedded.Items {
|
||||
for _, img := range wp.Embedded.Reference.Embedded.Images.Embedded.Items {
|
||||
photo, err := fetchPhoto(img.Src, "", "")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -418,7 +452,7 @@ func fetchWaypointPhotos(wp Item) ([]*filesystem.File, error) {
|
||||
if strings.HasSuffix(photo.Name, ".gif") {
|
||||
continue
|
||||
}
|
||||
photos[i] = photo
|
||||
photos = append(photos, photo)
|
||||
}
|
||||
|
||||
return photos, nil
|
||||
@@ -438,35 +472,36 @@ func fetchPhoto(url string, width string, height string) (*filesystem.File, erro
|
||||
}
|
||||
|
||||
func generateTourGPX(detailedTour *DetailedKomootTour) (*filesystem.File, error) {
|
||||
var points []*gpx.WptType
|
||||
var points []gpx.GPXPoint
|
||||
|
||||
for _, item := range detailedTour.Embedded.Coordinates.Items {
|
||||
t := detailedTour.Date.Unix() + int64(item.T/1000)
|
||||
|
||||
points = append(points, &gpx.WptType{Lat: item.Lat, Lon: item.Lng, Ele: item.Alt, Time: time.Unix(t, 0)})
|
||||
points = append(points, gpx.GPXPoint{
|
||||
Point: gpx.Point{Latitude: item.Lat, Longitude: item.Lng, Elevation: *gpx.NewNullableFloat64(item.Alt)},
|
||||
Timestamp: time.Unix(t, 0)})
|
||||
}
|
||||
|
||||
gpx := &gpx.GPX{
|
||||
gpxData := &gpx.GPX{
|
||||
Version: "1.1",
|
||||
Creator: "komoot GPX Exporter",
|
||||
Trk: []*gpx.TrkType{
|
||||
Tracks: []gpx.GPXTrack{
|
||||
{
|
||||
Name: detailedTour.Name,
|
||||
TrkSeg: []*gpx.TrkSegType{
|
||||
Segments: []gpx.GPXTrackSegment{
|
||||
{
|
||||
TrkPt: points,
|
||||
Points: points,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
var buf bytes.Buffer
|
||||
err := gpx.Write(&buf)
|
||||
gpxAsXML, err := gpxData.ToXml(gpx.ToXmlParams{Version: "1.1", Indent: true})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
gpxFile, err := filesystem.NewFileFromBytes(buf.Bytes(), detailedTour.Name+".gpx")
|
||||
gpxFile, err := filesystem.NewFileFromBytes(gpxAsXML, detailedTour.Name+".gpx")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -1,13 +1,19 @@
|
||||
package komoot
|
||||
|
||||
import "time"
|
||||
import (
|
||||
"time"
|
||||
|
||||
"pocketbase/services/trailmerge"
|
||||
)
|
||||
|
||||
type KomootIntegration struct {
|
||||
Active bool `json:"active"`
|
||||
Email string `json:"email"`
|
||||
Password string `json:"password"`
|
||||
Planned bool `json:"planned"`
|
||||
Completed bool `json:"completed"`
|
||||
Active bool `json:"active"`
|
||||
Email string `json:"email"`
|
||||
Password string `json:"password"`
|
||||
Planned bool `json:"planned"`
|
||||
Completed bool `json:"completed"`
|
||||
Privacy string `json:"privacy"`
|
||||
Merge trailmerge.IntegrationAutoMergeSettings `json:"merge"`
|
||||
}
|
||||
|
||||
type LoginResponse struct {
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
package strava
|
||||
|
||||
import "time"
|
||||
import (
|
||||
"time"
|
||||
|
||||
"pocketbase/services/trailmerge"
|
||||
)
|
||||
|
||||
type TokenRequest struct {
|
||||
ClientID int32 `json:"client_id"`
|
||||
@@ -21,14 +25,17 @@ type RefreshTokenResponse struct {
|
||||
ExpiresAt int64 `json:"expires_at"`
|
||||
}
|
||||
type StravaIntegration struct {
|
||||
Active bool `json:"active"`
|
||||
Routes bool `json:"routes"`
|
||||
Activities bool `json:"activities"`
|
||||
ClientID int32 `json:"clientId"`
|
||||
ClientSecret string `json:"clientSecret"`
|
||||
AccessToken string `json:"accessToken,omitempty"`
|
||||
RefreshToken string `json:"refreshToken,omitempty"`
|
||||
ExpiresAt int64 `json:"expiresAt,omitempty"`
|
||||
Active bool `json:"active"`
|
||||
Routes bool `json:"routes"`
|
||||
Activities bool `json:"activities"`
|
||||
ClientID int32 `json:"clientId"`
|
||||
ClientSecret string `json:"clientSecret"`
|
||||
AccessToken string `json:"accessToken,omitempty"`
|
||||
RefreshToken string `json:"refreshToken,omitempty"`
|
||||
ExpiresAt int64 `json:"expiresAt,omitempty"`
|
||||
Privacy string `json:"privacy"`
|
||||
After string `json:"after,omitempty"`
|
||||
Merge trailmerge.IntegrationAutoMergeSettings `json:"merge"`
|
||||
}
|
||||
type StravaRoute struct {
|
||||
Athlete Athlete `json:"athlete"`
|
||||
|
||||
@@ -2,6 +2,7 @@ package strava
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
@@ -11,19 +12,23 @@ import (
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/meilisearch/meilisearch-go"
|
||||
"github.com/pocketbase/dbx"
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
"github.com/pocketbase/pocketbase/tools/filesystem"
|
||||
"github.com/pocketbase/pocketbase/tools/security"
|
||||
"github.com/twpayne/go-gpx"
|
||||
"github.com/tkrajina/gpxgo/gpx"
|
||||
"github.com/twpayne/go-polyline"
|
||||
|
||||
"pocketbase/services/trailmerge"
|
||||
"pocketbase/util"
|
||||
)
|
||||
|
||||
type StravaApi struct {
|
||||
AceessToken string
|
||||
}
|
||||
|
||||
func SyncStrava(app core.App) error {
|
||||
func SyncStrava(app core.App, client meilisearch.ServiceManager) error {
|
||||
integrations, err := app.FindAllRecords("integrations", dbx.NewExp("true"))
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -43,7 +48,11 @@ func SyncStrava(app core.App) error {
|
||||
app.Logger().Warn(warning)
|
||||
continue
|
||||
}
|
||||
actorId := actor.Id
|
||||
|
||||
ctx, err := util.GetSafeActorContext(nil, actor)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
stravaString := i.GetString("strava")
|
||||
var stravaIntegration StravaIntegration
|
||||
@@ -89,22 +98,12 @@ func SyncStrava(app core.App) error {
|
||||
stravaIntegration.ExpiresAt = r.ExpiresAt
|
||||
}
|
||||
|
||||
b, err := json.Marshal(stravaIntegration)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
i.Set("strava", string(b))
|
||||
err = app.Save(i)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if stravaIntegration.Routes {
|
||||
page := 1
|
||||
hasNewRoutes := true
|
||||
for hasNewRoutes {
|
||||
|
||||
hasMore := true
|
||||
for hasMore {
|
||||
routes, err := fetchStravaRoutes(r.AccessToken, page)
|
||||
hasMore = len(routes) > 0
|
||||
page += 1
|
||||
if err != nil {
|
||||
warning := fmt.Sprintf("error fetching routes from strava: %v\n", err)
|
||||
@@ -112,7 +111,7 @@ func SyncStrava(app core.App) error {
|
||||
app.Logger().Warn(warning)
|
||||
break
|
||||
}
|
||||
hasNewRoutes, err = syncTrailsWithRoutes(app, r.AccessToken, userId, actorId, routes)
|
||||
err = syncTrailsWithRoutes(app, client, ctx, stravaIntegration, r.AccessToken, userId, actor, routes)
|
||||
if err != nil {
|
||||
warning := fmt.Sprintf("error syncing strava routes with trails: %v\n", err)
|
||||
fmt.Print(warning)
|
||||
@@ -123,9 +122,20 @@ func SyncStrava(app core.App) error {
|
||||
}
|
||||
if stravaIntegration.Activities {
|
||||
page := 1
|
||||
hasNewActivities := true
|
||||
for hasNewActivities {
|
||||
activities, err := fetchStravaActivities(r.AccessToken, page)
|
||||
hasMore := true
|
||||
for hasMore {
|
||||
var after int64 = 0
|
||||
if stravaIntegration.After != "" {
|
||||
t, err := time.Parse("2006-01-02", stravaIntegration.After)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
t = t.UTC()
|
||||
|
||||
after = t.Unix()
|
||||
}
|
||||
activities, err := fetchStravaActivities(r.AccessToken, page, after)
|
||||
hasMore = len(activities) > 0
|
||||
page += 1
|
||||
if err != nil {
|
||||
warning := fmt.Sprintf("error fetching activities from strava: %v", err)
|
||||
@@ -133,7 +143,8 @@ func SyncStrava(app core.App) error {
|
||||
app.Logger().Warn(warning)
|
||||
break
|
||||
}
|
||||
hasNewActivities, err = syncTrailsWithActivities(app, r.AccessToken, userId, actorId, activities)
|
||||
err = syncTrailsWithActivities(app, client, ctx, stravaIntegration, r.AccessToken, userId, actor, activities)
|
||||
|
||||
if err != nil {
|
||||
warning := fmt.Sprintf("error syncing strava activities with trails: %v", err)
|
||||
fmt.Print(warning)
|
||||
@@ -141,6 +152,17 @@ func SyncStrava(app core.App) error {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
b, err := json.Marshal(stravaIntegration)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
i.Set("strava", string(b))
|
||||
err = app.Save(i)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
@@ -208,8 +230,8 @@ func fetchStravaRoutes(accessToken string, page int) ([]StravaRoute, error) {
|
||||
return routes, nil
|
||||
}
|
||||
|
||||
func fetchStravaActivities(accessToken string, page int) ([]StravaActivity, error) {
|
||||
stravaRoutesURL := fmt.Sprintf("https://www.strava.com/api/v3/athlete/activities?page=%d", page)
|
||||
func fetchStravaActivities(accessToken string, page int, after int64) ([]StravaActivity, error) {
|
||||
stravaRoutesURL := fmt.Sprintf("https://www.strava.com/api/v3/athlete/activities?page=%d&after=%d", page, after)
|
||||
req, err := http.NewRequest("GET", stravaRoutesURL, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -235,36 +257,36 @@ func fetchStravaActivities(accessToken string, page int) ([]StravaActivity, erro
|
||||
return activities, nil
|
||||
}
|
||||
|
||||
func syncTrailsWithRoutes(app core.App, accessToken string, user string, actor string, routes []StravaRoute) (bool, error) {
|
||||
hasNewRoutes := false
|
||||
func syncTrailsWithRoutes(app core.App, client meilisearch.ServiceManager, ctx context.Context, i StravaIntegration, accessToken string, user string, actor *core.Record, routes []StravaRoute) error {
|
||||
for _, route := range routes {
|
||||
trails, err := app.FindRecordsByFilter("trails", "external_id = {:id}", "", 1, 0, dbx.Params{"id": route.IDStr})
|
||||
existingTrail, err := util.FindTrailByExternalReference(app, "strava", route.IDStr)
|
||||
if err != nil {
|
||||
return hasNewRoutes, err
|
||||
return err
|
||||
}
|
||||
if len(trails) != 0 {
|
||||
if existingTrail != nil {
|
||||
continue
|
||||
}
|
||||
hasNewRoutes = true
|
||||
gpx, err := fetchRouteGPX(route, accessToken)
|
||||
if err != nil {
|
||||
app.Logger().Warn(fmt.Sprintf("Unable to fetch GPX for route '%s': %v", route.Name, err))
|
||||
continue
|
||||
}
|
||||
wpIds, err := createWaypointsFromRoute(app, route, user)
|
||||
if err != nil {
|
||||
app.Logger().Warn(fmt.Sprintf("Unable to create waypoints for route '%s': %v", route.Name, err))
|
||||
continue
|
||||
}
|
||||
err = createTrailFromRoute(app, route, gpx, actor, wpIds)
|
||||
trailid, err := createTrailFromRoute(app, route, gpx, user, actor.Id, i.Privacy)
|
||||
if err != nil {
|
||||
app.Logger().Warn(fmt.Sprintf("Unable to create trail for route '%s': %v", route.Name, err))
|
||||
continue
|
||||
}
|
||||
|
||||
err = createWaypointsFromRoute(app, route, user, trailid)
|
||||
if err != nil {
|
||||
app.Logger().Warn(fmt.Sprintf("Unable to create waypoints for route '%s': %v", route.Name, err))
|
||||
continue
|
||||
}
|
||||
if err := trailmerge.TryAutoMergeImportedTrail(app, client, ctx, actor, trailid, i.Merge); err != nil {
|
||||
app.Logger().Warn(fmt.Sprintf("Unable to auto-merge imported Strava route '%s': %v", route.Name, err))
|
||||
}
|
||||
}
|
||||
|
||||
return hasNewRoutes, nil
|
||||
return nil
|
||||
}
|
||||
|
||||
func fetchRouteGPX(route StravaRoute, accessToken string) (*filesystem.File, error) {
|
||||
@@ -305,10 +327,12 @@ func fetchRouteGPX(route StravaRoute, accessToken string) (*filesystem.File, err
|
||||
return gpxFile, nil
|
||||
}
|
||||
|
||||
func createTrailFromRoute(app core.App, route StravaRoute, gpx *filesystem.File, actor string, wpIds []string) error {
|
||||
func createTrailFromRoute(app core.App, route StravaRoute, gpx *filesystem.File, user string, actor string, privacy string) (string, error) {
|
||||
trailid := security.RandomStringWithAlphabet(core.DefaultIdLength, core.DefaultIdAlphabet)
|
||||
|
||||
collection, err := app.FindCollectionByNameOrId("trails")
|
||||
if err != nil {
|
||||
return err
|
||||
return "", err
|
||||
}
|
||||
|
||||
record := core.NewRecord(collection)
|
||||
@@ -336,22 +360,36 @@ func createTrailFromRoute(app core.App, route StravaRoute, gpx *filesystem.File,
|
||||
category = hikeCategory.Id
|
||||
}
|
||||
|
||||
public := !route.Private
|
||||
|
||||
if privacy == "settings" {
|
||||
privacySettings := struct {
|
||||
Trails string `json:"trails"`
|
||||
}{}
|
||||
|
||||
settings, _ := app.FindFirstRecordByData("settings", "user", user)
|
||||
err = settings.UnmarshalJSONField("privacy", &privacySettings)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
public = privacySettings.Trails == "public"
|
||||
}
|
||||
|
||||
record.Load(map[string]any{
|
||||
"name": route.Name,
|
||||
"description": route.Description,
|
||||
"public": !route.Private,
|
||||
"distance": route.Distance,
|
||||
"elevation_gain": route.ElevationGain,
|
||||
"duration": route.EstimatedMovingTime,
|
||||
"date": time.Unix(int64(route.Timestamp), 0),
|
||||
"external_provider": "strava",
|
||||
"external_id": route.IDStr,
|
||||
"lat": lat,
|
||||
"lon": lon,
|
||||
"waypoints": wpIds,
|
||||
"difficulty": "easy",
|
||||
"category": category,
|
||||
"author": actor,
|
||||
"id": trailid,
|
||||
"name": route.Name,
|
||||
"description": route.Description,
|
||||
"public": public,
|
||||
"distance": route.Distance,
|
||||
"elevation_gain": route.ElevationGain,
|
||||
"duration": route.EstimatedMovingTime,
|
||||
"date": time.Unix(int64(route.Timestamp), 0),
|
||||
"lat": lat,
|
||||
"lon": lon,
|
||||
"difficulty": "easy",
|
||||
"category": category,
|
||||
"author": actor,
|
||||
})
|
||||
|
||||
if gpx != nil {
|
||||
@@ -359,20 +397,21 @@ func createTrailFromRoute(app core.App, route StravaRoute, gpx *filesystem.File,
|
||||
}
|
||||
|
||||
if err := app.Save(record); err != nil {
|
||||
return err
|
||||
return "", err
|
||||
}
|
||||
if err := util.EnsureTrailExternalReference(app, trailid, "strava", route.IDStr); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return nil
|
||||
return trailid, err
|
||||
}
|
||||
|
||||
func createWaypointsFromRoute(app core.App, route StravaRoute, user string) ([]string, error) {
|
||||
func createWaypointsFromRoute(app core.App, route StravaRoute, user string, trailid string) error {
|
||||
collection, err := app.FindCollectionByNameOrId("waypoints")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return err
|
||||
}
|
||||
|
||||
wpIds := make([]string, len(route.Waypoints))
|
||||
|
||||
for i, wp := range route.Waypoints {
|
||||
record := core.NewRecord(collection)
|
||||
|
||||
@@ -383,26 +422,26 @@ func createWaypointsFromRoute(app core.App, route StravaRoute, user string) ([]s
|
||||
record.Set("icon", "circle")
|
||||
record.Set("author", user)
|
||||
record.Set("distance_from_start", wp.DistanceIntoRoute)
|
||||
record.Set("trail", trailid)
|
||||
|
||||
app.Save(record)
|
||||
if err := app.Save(record); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
wpIds[i] = record.Id
|
||||
}
|
||||
|
||||
return wpIds, nil
|
||||
return nil
|
||||
}
|
||||
|
||||
func syncTrailsWithActivities(app core.App, accessToken string, user string, actor string, activities []StravaActivity) (bool, error) {
|
||||
hasNewActivites := false
|
||||
func syncTrailsWithActivities(app core.App, client meilisearch.ServiceManager, ctx context.Context, i StravaIntegration, accessToken string, user string, actor *core.Record, activities []StravaActivity) error {
|
||||
for _, activity := range activities {
|
||||
trails, err := app.FindRecordsByFilter("trails", "external_id = {:id}", "", 1, 0, dbx.Params{"id": strconv.Itoa(int(activity.ID))})
|
||||
existingTrail, err := util.FindTrailByExternalReference(app, "strava", strconv.Itoa(int(activity.ID)))
|
||||
if err != nil {
|
||||
return hasNewActivites, err
|
||||
return err
|
||||
}
|
||||
if len(trails) != 0 {
|
||||
if existingTrail != nil {
|
||||
continue
|
||||
}
|
||||
hasNewActivites = true
|
||||
detailedActivity, err := fetchDetailedActivity(activity, accessToken)
|
||||
if err != nil {
|
||||
app.Logger().Warn(fmt.Sprintf("Unable to fetch detailed activity '%s': %v", activity.Name, err))
|
||||
@@ -413,15 +452,17 @@ func syncTrailsWithActivities(app core.App, accessToken string, user string, act
|
||||
app.Logger().Warn(fmt.Sprintf("Unable to fetch GPX for activity '%s': %v", activity.Name, err))
|
||||
continue
|
||||
}
|
||||
err = createTrailFromActivity(app, detailedActivity, gpx, actor)
|
||||
trailID, err := createTrailFromActivity(app, detailedActivity, gpx, user, actor.Id, i.Privacy)
|
||||
if err != nil {
|
||||
app.Logger().Warn(fmt.Sprintf("Unable to create trail from activity '%s': %v", activity.Name, err))
|
||||
continue
|
||||
}
|
||||
|
||||
if err := trailmerge.TryAutoMergeImportedTrail(app, client, ctx, actor, trailID, i.Merge); err != nil {
|
||||
app.Logger().Warn(fmt.Sprintf("Unable to auto-merge imported Strava activity '%s': %v", activity.Name, err))
|
||||
}
|
||||
}
|
||||
|
||||
return hasNewActivites, nil
|
||||
return nil
|
||||
}
|
||||
|
||||
func fetchDetailedActivity(activity StravaActivity, accessToken string) (*DetailedStravaActivity, error) {
|
||||
@@ -451,21 +492,21 @@ func fetchDetailedActivity(activity StravaActivity, accessToken string) (*Detail
|
||||
return &detailedActivity, nil
|
||||
}
|
||||
|
||||
func createTrailFromActivity(app core.App, activity *DetailedStravaActivity, gpx *filesystem.File, user string) error {
|
||||
func createTrailFromActivity(app core.App, activity *DetailedStravaActivity, gpx *filesystem.File, user string, actor string, privacy string) (string, error) {
|
||||
if len(activity.StartLatlng) < 2 {
|
||||
return nil
|
||||
return "", nil
|
||||
}
|
||||
|
||||
collection, err := app.FindCollectionByNameOrId("trails")
|
||||
if err != nil {
|
||||
return err
|
||||
return "", err
|
||||
}
|
||||
|
||||
var photo *filesystem.File
|
||||
if len(activity.Photos.Primary.Urls.Num600) > 0 {
|
||||
photo, err = fetchActivityPhoto(activity)
|
||||
if err != nil {
|
||||
return err
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
|
||||
@@ -517,21 +558,35 @@ func createTrailFromActivity(app core.App, activity *DetailedStravaActivity, gpx
|
||||
categoryId = category.Id
|
||||
}
|
||||
|
||||
public := !activity.Private
|
||||
|
||||
if privacy == "settings" {
|
||||
privacySettings := struct {
|
||||
Trails string `json:"trails"`
|
||||
}{}
|
||||
|
||||
settings, _ := app.FindFirstRecordByData("settings", "user", user)
|
||||
err = settings.UnmarshalJSONField("privacy", &privacySettings)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
public = privacySettings.Trails == "public"
|
||||
}
|
||||
|
||||
record.Load(map[string]any{
|
||||
"name": activity.Name,
|
||||
"description": activity.Description,
|
||||
"public": !activity.Private,
|
||||
"distance": activity.Distance,
|
||||
"elevation_gain": activity.TotalElevationGain,
|
||||
"duration": activity.ElapsedTime,
|
||||
"date": activity.StartDate,
|
||||
"external_provider": "strava",
|
||||
"external_id": activity.ID,
|
||||
"lat": activity.StartLatlng[0],
|
||||
"lon": activity.StartLatlng[1],
|
||||
"difficulty": "easy",
|
||||
"category": categoryId,
|
||||
"author": user,
|
||||
"name": activity.Name,
|
||||
"description": activity.Description,
|
||||
"public": public,
|
||||
"distance": activity.Distance,
|
||||
"elevation_gain": activity.TotalElevationGain,
|
||||
"duration": activity.ElapsedTime,
|
||||
"date": activity.StartDate,
|
||||
"lat": activity.StartLatlng[0],
|
||||
"lon": activity.StartLatlng[1],
|
||||
"difficulty": "easy",
|
||||
"category": categoryId,
|
||||
"author": actor,
|
||||
})
|
||||
|
||||
if photo != nil {
|
||||
@@ -543,10 +598,13 @@ func createTrailFromActivity(app core.App, activity *DetailedStravaActivity, gpx
|
||||
}
|
||||
|
||||
if err := app.Save(record); err != nil {
|
||||
return err
|
||||
return "", err
|
||||
}
|
||||
if err := util.EnsureTrailExternalReference(app, record.Id, "strava", strconv.Itoa(int(activity.ID))); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return nil
|
||||
return record.Id, nil
|
||||
}
|
||||
|
||||
func fetchActivityPhoto(activity *DetailedStravaActivity) (*filesystem.File, error) {
|
||||
@@ -610,7 +668,7 @@ func generateActivityGPX(activity *DetailedStravaActivity, accessToken string) (
|
||||
timeStream := streamResponse.Time
|
||||
altitudeStream := streamResponse.Altitude
|
||||
|
||||
var points []*gpx.WptType
|
||||
var points []gpx.GPXPoint
|
||||
|
||||
for i, latlng := range latLngStream.Data {
|
||||
lat := latlng[0]
|
||||
@@ -618,30 +676,31 @@ func generateActivityGPX(activity *DetailedStravaActivity, accessToken string) (
|
||||
alt := altitudeStream.Data[i]
|
||||
t := activity.StartDate.Unix() + int64(timeStream.Data[i])
|
||||
|
||||
points = append(points, &gpx.WptType{Lat: lat, Lon: lon, Ele: alt, Time: time.Unix(t, 0)})
|
||||
points = append(points, gpx.GPXPoint{
|
||||
Point: gpx.Point{Latitude: lat, Longitude: lon, Elevation: *gpx.NewNullableFloat64(alt)},
|
||||
Timestamp: time.Unix(t, 0)})
|
||||
}
|
||||
|
||||
gpx := &gpx.GPX{
|
||||
gpxData := &gpx.GPX{
|
||||
Version: "1.1",
|
||||
Creator: "Strava GPX Exporter",
|
||||
Trk: []*gpx.TrkType{
|
||||
Tracks: []gpx.GPXTrack{
|
||||
{
|
||||
Name: activity.Name,
|
||||
TrkSeg: []*gpx.TrkSegType{
|
||||
Segments: []gpx.GPXTrackSegment{
|
||||
{
|
||||
TrkPt: points,
|
||||
Points: points,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
var buf bytes.Buffer
|
||||
err = gpx.Write(&buf)
|
||||
gpxAsXML, err := gpxData.ToXml(gpx.ToXmlParams{Version: "1.1", Indent: true})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
gpxFile, err := filesystem.NewFileFromBytes(buf.Bytes(), activity.Name+".gpx")
|
||||
gpxFile, err := filesystem.NewFileFromBytes(gpxAsXML, activity.Name+".gpx")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
1324
db/main.go
1324
db/main.go
File diff suppressed because it is too large
Load Diff
@@ -28,7 +28,7 @@ func init() {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = client.Index("trails").UpdateFilterableAttributes(&[]string{
|
||||
_, err = client.Index("trails").UpdateFilterableAttributes(&[]interface{}{
|
||||
"_geo", "author", "category", "completed", "date", "difficulty", "distance", "elevation_gain", "elevation_loss", "public", "shares", "tags",
|
||||
})
|
||||
|
||||
@@ -53,7 +53,7 @@ func init() {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = client.Index("lists").UpdateFilterableAttributes(&[]string{
|
||||
_, err = client.Index("lists").UpdateFilterableAttributes(&[]interface{}{
|
||||
"author", "public", "shares",
|
||||
})
|
||||
if err != nil {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -20,7 +20,7 @@ func init() {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = client.Index("trails").UpdateFilterableAttributes(&[]string{
|
||||
_, err = client.Index("trails").UpdateFilterableAttributes(&[]any{
|
||||
"_geo", "author", "category", "completed", "date", "difficulty", "distance", "elevation_gain", "elevation_loss", "public", "shares", "tags", "likes",
|
||||
})
|
||||
|
||||
@@ -33,7 +33,7 @@ func init() {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = client.Index("trails").UpdateFilterableAttributes(&[]string{
|
||||
_, err = client.Index("trails").UpdateFilterableAttributes(&[]any{
|
||||
"_geo", "author", "category", "completed", "date", "difficulty", "distance", "elevation_gain", "elevation_loss", "public", "shares", "tags",
|
||||
})
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
115
db/migrations/1752231390_created_feed.go
Normal file
115
db/migrations/1752231390_created_feed.go
Normal file
@@ -0,0 +1,115 @@
|
||||
package migrations
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
m "github.com/pocketbase/pocketbase/migrations"
|
||||
)
|
||||
|
||||
func init() {
|
||||
m.Register(func(app core.App) error {
|
||||
jsonData := `{
|
||||
"createRule": null,
|
||||
"deleteRule": null,
|
||||
"fields": [
|
||||
{
|
||||
"autogeneratePattern": "[a-z0-9]{15}",
|
||||
"hidden": false,
|
||||
"id": "text3208210256",
|
||||
"max": 15,
|
||||
"min": 15,
|
||||
"name": "id",
|
||||
"pattern": "^[a-z0-9]+$",
|
||||
"presentable": false,
|
||||
"primaryKey": true,
|
||||
"required": true,
|
||||
"system": true,
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"cascadeDelete": true,
|
||||
"collectionId": "pbc_1295301207",
|
||||
"hidden": false,
|
||||
"id": "relation1148540665",
|
||||
"maxSelect": 1,
|
||||
"minSelect": 0,
|
||||
"name": "actor",
|
||||
"presentable": false,
|
||||
"required": true,
|
||||
"system": false,
|
||||
"type": "relation"
|
||||
},
|
||||
{
|
||||
"cascadeDelete": true,
|
||||
"collectionId": "pbc_1295301207",
|
||||
"hidden": false,
|
||||
"id": "relation3182418120",
|
||||
"maxSelect": 1,
|
||||
"minSelect": 0,
|
||||
"name": "author",
|
||||
"presentable": false,
|
||||
"required": true,
|
||||
"system": false,
|
||||
"type": "relation"
|
||||
},
|
||||
{
|
||||
"autogeneratePattern": "",
|
||||
"hidden": false,
|
||||
"id": "text521872670",
|
||||
"max": 15,
|
||||
"min": 15,
|
||||
"name": "item",
|
||||
"pattern": "^[a-z0-9]+$",
|
||||
"presentable": false,
|
||||
"primaryKey": false,
|
||||
"required": true,
|
||||
"system": false,
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"hidden": false,
|
||||
"id": "autodate2990389176",
|
||||
"name": "created",
|
||||
"onCreate": true,
|
||||
"onUpdate": false,
|
||||
"presentable": false,
|
||||
"system": false,
|
||||
"type": "autodate"
|
||||
},
|
||||
{
|
||||
"hidden": false,
|
||||
"id": "autodate3332085495",
|
||||
"name": "updated",
|
||||
"onCreate": true,
|
||||
"onUpdate": true,
|
||||
"presentable": false,
|
||||
"system": false,
|
||||
"type": "autodate"
|
||||
}
|
||||
],
|
||||
"id": "pbc_72164123",
|
||||
"indexes": [],
|
||||
"listRule": null,
|
||||
"name": "feed",
|
||||
"system": false,
|
||||
"type": "base",
|
||||
"updateRule": null,
|
||||
"viewRule": null
|
||||
}`
|
||||
|
||||
collection := &core.Collection{}
|
||||
if err := json.Unmarshal([]byte(jsonData), &collection); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return app.Save(collection)
|
||||
}, func(app core.App) error {
|
||||
collection, err := app.FindCollectionByNameOrId("pbc_72164123")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return app.Delete(collection)
|
||||
})
|
||||
}
|
||||
46
db/migrations/1752232212_updated_feed.go
Normal file
46
db/migrations/1752232212_updated_feed.go
Normal file
@@ -0,0 +1,46 @@
|
||||
package migrations
|
||||
|
||||
import (
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
m "github.com/pocketbase/pocketbase/migrations"
|
||||
)
|
||||
|
||||
func init() {
|
||||
m.Register(func(app core.App) error {
|
||||
collection, err := app.FindCollectionByNameOrId("pbc_72164123")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// add field
|
||||
if err := collection.Fields.AddMarshaledJSONAt(4, []byte(`{
|
||||
"hidden": false,
|
||||
"id": "select2363381545",
|
||||
"maxSelect": 1,
|
||||
"name": "type",
|
||||
"presentable": false,
|
||||
"required": true,
|
||||
"system": false,
|
||||
"type": "select",
|
||||
"values": [
|
||||
"trail",
|
||||
"list",
|
||||
"summit_log"
|
||||
]
|
||||
}`)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return app.Save(collection)
|
||||
}, func(app core.App) error {
|
||||
collection, err := app.FindCollectionByNameOrId("pbc_72164123")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// remove field
|
||||
collection.Fields.RemoveById("select2363381545")
|
||||
|
||||
return app.Save(collection)
|
||||
})
|
||||
}
|
||||
40
db/migrations/1752233467_updated_feed.go
Normal file
40
db/migrations/1752233467_updated_feed.go
Normal file
@@ -0,0 +1,40 @@
|
||||
package migrations
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
m "github.com/pocketbase/pocketbase/migrations"
|
||||
)
|
||||
|
||||
func init() {
|
||||
m.Register(func(app core.App) error {
|
||||
collection, err := app.FindCollectionByNameOrId("pbc_72164123")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// update collection data
|
||||
if err := json.Unmarshal([]byte(`{
|
||||
"listRule": "actor.user = @request.auth.id"
|
||||
}`), &collection); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return app.Save(collection)
|
||||
}, func(app core.App) error {
|
||||
collection, err := app.FindCollectionByNameOrId("pbc_72164123")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// update collection data
|
||||
if err := json.Unmarshal([]byte(`{
|
||||
"listRule": null
|
||||
}`), &collection); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return app.Save(collection)
|
||||
})
|
||||
}
|
||||
100
db/migrations/1752239359_create_initial_feed.go
Normal file
100
db/migrations/1752239359_create_initial_feed.go
Normal file
@@ -0,0 +1,100 @@
|
||||
package migrations
|
||||
|
||||
import (
|
||||
"pocketbase/util"
|
||||
|
||||
"github.com/pocketbase/dbx"
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
m "github.com/pocketbase/pocketbase/migrations"
|
||||
)
|
||||
|
||||
func init() {
|
||||
m.Register(func(app core.App) error {
|
||||
trails, err := app.FindAllRecords("trails")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, t := range trails {
|
||||
trailAuthor, err := app.FindRecordById("activitypub_actors", t.GetString("author"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
feed, err := util.InsertIntoFeed(app, trailAuthor.Id, trailAuthor.Id, t.Id, util.TrailFeed)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
feed.SetRaw("created", t.GetDateTime("created"))
|
||||
err = app.Save(feed)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
followers, err := app.FindRecordsByFilter("follows", "followee={:author}", "", -1, 0, dbx.Params{"author": trailAuthor.Id})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, f := range followers {
|
||||
feed, err = util.InsertIntoFeed(app, f.GetString("follower"), trailAuthor.Id, t.Id, util.TrailFeed)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
feed.SetRaw("created", t.GetDateTime("created"))
|
||||
err = app.Save(feed)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
lists, err := app.FindAllRecords("lists")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, t := range lists {
|
||||
listAuthor, err := app.FindRecordById("activitypub_actors", t.GetString("author"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
feed, err := util.InsertIntoFeed(app, listAuthor.Id, listAuthor.Id, t.Id, util.ListFeed)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
feed.SetRaw("created", t.GetDateTime("created"))
|
||||
err = app.Save(feed)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
followers, err := app.FindRecordsByFilter("follows", "followee={:author}", "", -1, 0, dbx.Params{"author": listAuthor.Id})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, f := range followers {
|
||||
feed, err = util.InsertIntoFeed(app, f.GetString("follower"), listAuthor.Id, t.Id, util.ListFeed)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
feed.SetRaw("created", t.GetDateTime("created"))
|
||||
err = app.Save(feed)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}, func(app core.App) error {
|
||||
_, err := app.DB().
|
||||
NewQuery("DELETE FROM feed;").
|
||||
Execute()
|
||||
|
||||
return err
|
||||
})
|
||||
}
|
||||
106
db/migrations/1752321031_created_profile_feed.go
Normal file
106
db/migrations/1752321031_created_profile_feed.go
Normal file
@@ -0,0 +1,106 @@
|
||||
package migrations
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
m "github.com/pocketbase/pocketbase/migrations"
|
||||
)
|
||||
|
||||
func init() {
|
||||
m.Register(func(app core.App) error {
|
||||
jsonData := `{
|
||||
"createRule": null,
|
||||
"deleteRule": null,
|
||||
"fields": [
|
||||
{
|
||||
"autogeneratePattern": "",
|
||||
"hidden": false,
|
||||
"id": "text3208210256",
|
||||
"max": 0,
|
||||
"min": 0,
|
||||
"name": "id",
|
||||
"pattern": "^[a-z0-9]+$",
|
||||
"presentable": false,
|
||||
"primaryKey": true,
|
||||
"required": true,
|
||||
"system": true,
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"hidden": false,
|
||||
"id": "json1148540665",
|
||||
"maxSize": 1,
|
||||
"name": "actor",
|
||||
"presentable": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "json"
|
||||
},
|
||||
{
|
||||
"hidden": false,
|
||||
"id": "json3182418120",
|
||||
"maxSize": 1,
|
||||
"name": "author",
|
||||
"presentable": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "json"
|
||||
},
|
||||
{
|
||||
"hidden": false,
|
||||
"id": "json521872670",
|
||||
"maxSize": 1,
|
||||
"name": "item",
|
||||
"presentable": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "json"
|
||||
},
|
||||
{
|
||||
"hidden": false,
|
||||
"id": "json2363381545",
|
||||
"maxSize": 1,
|
||||
"name": "type",
|
||||
"presentable": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "json"
|
||||
},
|
||||
{
|
||||
"hidden": false,
|
||||
"id": "json2990389176",
|
||||
"maxSize": 1,
|
||||
"name": "created",
|
||||
"presentable": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "json"
|
||||
}
|
||||
],
|
||||
"id": "pbc_1973704172",
|
||||
"indexes": [],
|
||||
"listRule": "",
|
||||
"name": "profile_feed",
|
||||
"system": false,
|
||||
"type": "view",
|
||||
"updateRule": null,
|
||||
"viewQuery": "SELECT\n (ROW_NUMBER() OVER ()) as id,\n actor,\n author,\n item,\n type,\n created\nFROM\n (\n SELECT\n author as actor,\n author,\n id as item,\n \"list\" as type,\n created\n FROM\n lists\n UNION\n SELECT\n author as actor,\n author,\n id as item,\n \"trail\" as type,\n created\n FROM trails\n )\nORDER BY created desc;",
|
||||
"viewRule": null
|
||||
}`
|
||||
|
||||
collection := &core.Collection{}
|
||||
if err := json.Unmarshal([]byte(jsonData), &collection); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return app.Save(collection)
|
||||
}, func(app core.App) error {
|
||||
collection, err := app.FindCollectionByNameOrId("pbc_1973704172")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return app.Delete(collection)
|
||||
})
|
||||
}
|
||||
57
db/migrations/1752324952_updated_profile_feed.go
Normal file
57
db/migrations/1752324952_updated_profile_feed.go
Normal file
@@ -0,0 +1,57 @@
|
||||
package migrations
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
m "github.com/pocketbase/pocketbase/migrations"
|
||||
)
|
||||
|
||||
func init() {
|
||||
m.Register(func(app core.App) error {
|
||||
collection, err := app.FindCollectionByNameOrId("pbc_1973704172")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// update collection data
|
||||
if err := json.Unmarshal([]byte(`{
|
||||
"viewQuery": "SELECT\n (ROW_NUMBER() OVER ()) as id,\n actor,\n item,\n type,\n created\nFROM\n (\n SELECT\n author as actor,\n id as item,\n 'list' as type,\n created\n FROM\n lists\n UNION\n SELECT\n author as actor,\n id as item,\n 'trail' as type,\n created\n FROM trails\n )\nORDER BY created desc;"
|
||||
}`), &collection); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// remove field
|
||||
collection.Fields.RemoveById("json3182418120")
|
||||
|
||||
return app.Save(collection)
|
||||
}, func(app core.App) error {
|
||||
collection, err := app.FindCollectionByNameOrId("pbc_1973704172")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// update collection data
|
||||
if err := json.Unmarshal([]byte(`{
|
||||
"viewQuery": "SELECT\n (ROW_NUMBER() OVER ()) as id,\n actor,\n author,\n item,\n type,\n created\nFROM\n (\n SELECT\n author as actor,\n author,\n id as item,\n \"list\" as type,\n created\n FROM\n lists\n UNION\n SELECT\n author as actor,\n author,\n id as item,\n \"trail\" as type,\n created\n FROM trails\n )\nORDER BY created desc;"
|
||||
}`), &collection); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// add field
|
||||
if err := collection.Fields.AddMarshaledJSONAt(2, []byte(`{
|
||||
"hidden": false,
|
||||
"id": "json3182418120",
|
||||
"maxSize": 1,
|
||||
"name": "author",
|
||||
"presentable": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "json"
|
||||
}`)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return app.Save(collection)
|
||||
})
|
||||
}
|
||||
44
db/migrations/1752324970_updated_feed.go
Normal file
44
db/migrations/1752324970_updated_feed.go
Normal file
@@ -0,0 +1,44 @@
|
||||
package migrations
|
||||
|
||||
import (
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
m "github.com/pocketbase/pocketbase/migrations"
|
||||
)
|
||||
|
||||
func init() {
|
||||
m.Register(func(app core.App) error {
|
||||
collection, err := app.FindCollectionByNameOrId("pbc_72164123")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// remove field
|
||||
collection.Fields.RemoveById("relation3182418120")
|
||||
|
||||
return app.Save(collection)
|
||||
}, func(app core.App) error {
|
||||
collection, err := app.FindCollectionByNameOrId("pbc_72164123")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// add field
|
||||
if err := collection.Fields.AddMarshaledJSONAt(2, []byte(`{
|
||||
"cascadeDelete": true,
|
||||
"collectionId": "pbc_1295301207",
|
||||
"hidden": false,
|
||||
"id": "relation3182418120",
|
||||
"maxSelect": 1,
|
||||
"minSelect": 0,
|
||||
"name": "author",
|
||||
"presentable": false,
|
||||
"required": true,
|
||||
"system": false,
|
||||
"type": "relation"
|
||||
}`)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return app.Save(collection)
|
||||
})
|
||||
}
|
||||
216
db/migrations/1752330359_deleted_timeline.go
Normal file
216
db/migrations/1752330359_deleted_timeline.go
Normal file
@@ -0,0 +1,216 @@
|
||||
package migrations
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
m "github.com/pocketbase/pocketbase/migrations"
|
||||
)
|
||||
|
||||
func init() {
|
||||
m.Register(func(app core.App) error {
|
||||
collection, err := app.FindCollectionByNameOrId("pbc_468398817")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return app.Delete(collection)
|
||||
}, func(app core.App) error {
|
||||
jsonData := `{
|
||||
"createRule": null,
|
||||
"deleteRule": null,
|
||||
"fields": [
|
||||
{
|
||||
"autogeneratePattern": "",
|
||||
"hidden": false,
|
||||
"id": "text3208210256",
|
||||
"max": 0,
|
||||
"min": 0,
|
||||
"name": "id",
|
||||
"pattern": "^[a-z0-9]+$",
|
||||
"presentable": false,
|
||||
"primaryKey": true,
|
||||
"required": true,
|
||||
"system": true,
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"hidden": false,
|
||||
"id": "json2310347867",
|
||||
"maxSize": 1,
|
||||
"name": "trail_id",
|
||||
"presentable": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "json"
|
||||
},
|
||||
{
|
||||
"hidden": false,
|
||||
"id": "json3184124860",
|
||||
"maxSize": 1,
|
||||
"name": "trail_author_username",
|
||||
"presentable": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "json"
|
||||
},
|
||||
{
|
||||
"hidden": false,
|
||||
"id": "json2887874732",
|
||||
"maxSize": 1,
|
||||
"name": "trail_author_domain",
|
||||
"presentable": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "json"
|
||||
},
|
||||
{
|
||||
"hidden": false,
|
||||
"id": "json113557190",
|
||||
"maxSize": 1,
|
||||
"name": "trail_iri",
|
||||
"presentable": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "json"
|
||||
},
|
||||
{
|
||||
"hidden": false,
|
||||
"id": "json2862495610",
|
||||
"maxSize": 1,
|
||||
"name": "date",
|
||||
"presentable": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "json"
|
||||
},
|
||||
{
|
||||
"hidden": false,
|
||||
"id": "json1579384326",
|
||||
"maxSize": 1,
|
||||
"name": "name",
|
||||
"presentable": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "json"
|
||||
},
|
||||
{
|
||||
"hidden": false,
|
||||
"id": "json1843675174",
|
||||
"maxSize": 1,
|
||||
"name": "description",
|
||||
"presentable": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "json"
|
||||
},
|
||||
{
|
||||
"hidden": false,
|
||||
"id": "json3275261007",
|
||||
"maxSize": 1,
|
||||
"name": "gpx",
|
||||
"presentable": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "json"
|
||||
},
|
||||
{
|
||||
"hidden": false,
|
||||
"id": "json3182418120",
|
||||
"maxSize": 1,
|
||||
"name": "author",
|
||||
"presentable": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "json"
|
||||
},
|
||||
{
|
||||
"hidden": false,
|
||||
"id": "json142008537",
|
||||
"maxSize": 1,
|
||||
"name": "photos",
|
||||
"presentable": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "json"
|
||||
},
|
||||
{
|
||||
"hidden": false,
|
||||
"id": "json479369857",
|
||||
"maxSize": 1,
|
||||
"name": "distance",
|
||||
"presentable": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "json"
|
||||
},
|
||||
{
|
||||
"hidden": false,
|
||||
"id": "json2254405824",
|
||||
"maxSize": 1,
|
||||
"name": "duration",
|
||||
"presentable": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "json"
|
||||
},
|
||||
{
|
||||
"hidden": false,
|
||||
"id": "json3015100073",
|
||||
"maxSize": 1,
|
||||
"name": "elevation_gain",
|
||||
"presentable": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "json"
|
||||
},
|
||||
{
|
||||
"hidden": false,
|
||||
"id": "json3171089056",
|
||||
"maxSize": 1,
|
||||
"name": "elevation_loss",
|
||||
"presentable": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "json"
|
||||
},
|
||||
{
|
||||
"hidden": false,
|
||||
"id": "json2990389176",
|
||||
"maxSize": 1,
|
||||
"name": "created",
|
||||
"presentable": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "json"
|
||||
},
|
||||
{
|
||||
"hidden": false,
|
||||
"id": "json2363381545",
|
||||
"maxSize": 1,
|
||||
"name": "type",
|
||||
"presentable": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "json"
|
||||
}
|
||||
],
|
||||
"id": "pbc_468398817",
|
||||
"indexes": [],
|
||||
"listRule": "@collection.trails.id ?= trail_id && @collection.trails.public ?= true",
|
||||
"name": "timeline",
|
||||
"system": false,
|
||||
"type": "view",
|
||||
"updateRule": null,
|
||||
"viewQuery": "SELECT\n id,\n trail_id,\n trail_author_username,\n trail_author_domain,\n trail_iri,\n date,\n name,\n description,\n gpx,\n author,\n photos,\n distance,\n duration,\n elevation_gain,\n elevation_loss,\n created,\n type\nFROM\n (\n SELECT\n summit_logs.id,\n summit_logs.trail as trail_id,\n tapa.preferred_username as trail_author_username,\n tapa.domain as trail_author_domain,\n trails.iri as trail_iri,\n summit_logs.date,\n trails.name,\n text as description,\n summit_logs.gpx,\n sapa.iri as author,\n summit_logs.photos,\n summit_logs.distance,\n summit_logs.duration,\n summit_logs.elevation_gain,\n summit_logs.elevation_loss,\n summit_logs.created,\n \"summit_log\" as type\n FROM\n summit_logs\n JOIN trails ON summit_logs.trail = trails.id\n JOIN activitypub_actors sapa ON sapa.id = summit_logs.author\n JOIN activitypub_actors tapa ON tapa.id = trails.author\n UNION\n SELECT\n trails.id,\n trails.id as trail_id,\n activitypub_actors.preferred_username as trail_author_username,\n activitypub_actors.domain as trail_author_domain,\n trails.iri as trail_iri,\n date,\n trails.name,\n description,\n gpx,\n activitypub_actors.iri as author,\n photos,\n distance,\n duration,\n elevation_gain,\n elevation_loss,\n trails.created,\n \"trail\" as type\n FROM\n trails\n JOIN activitypub_actors ON activitypub_actors.id = trails.author\n )\nORDER BY\n created DESC;\n",
|
||||
"viewRule": "@collection.trails.id ?= trail_id && @collection.trails.public ?= true"
|
||||
}`
|
||||
|
||||
collection := &core.Collection{}
|
||||
if err := json.Unmarshal([]byte(jsonData), &collection); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return app.Save(collection)
|
||||
})
|
||||
}
|
||||
40
db/migrations/1752330401_updated_profile_feed.go
Normal file
40
db/migrations/1752330401_updated_profile_feed.go
Normal file
@@ -0,0 +1,40 @@
|
||||
package migrations
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
m "github.com/pocketbase/pocketbase/migrations"
|
||||
)
|
||||
|
||||
func init() {
|
||||
m.Register(func(app core.App) error {
|
||||
collection, err := app.FindCollectionByNameOrId("pbc_1973704172")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// update collection data
|
||||
if err := json.Unmarshal([]byte(`{
|
||||
"listRule": "actor.user.settings_via_user.privacy.account != 'private' || actor.user = @request.auth.id"
|
||||
}`), &collection); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return app.Save(collection)
|
||||
}, func(app core.App) error {
|
||||
collection, err := app.FindCollectionByNameOrId("pbc_1973704172")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// update collection data
|
||||
if err := json.Unmarshal([]byte(`{
|
||||
"listRule": ""
|
||||
}`), &collection); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return app.Save(collection)
|
||||
})
|
||||
}
|
||||
40
db/migrations/1752332025_updated_profile_feed.go
Normal file
40
db/migrations/1752332025_updated_profile_feed.go
Normal file
@@ -0,0 +1,40 @@
|
||||
package migrations
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
m "github.com/pocketbase/pocketbase/migrations"
|
||||
)
|
||||
|
||||
func init() {
|
||||
m.Register(func(app core.App) error {
|
||||
collection, err := app.FindCollectionByNameOrId("pbc_1973704172")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// update collection data
|
||||
if err := json.Unmarshal([]byte(`{
|
||||
"viewQuery": "SELECT\n (ROW_NUMBER() OVER ()) as id,\n actor,\n item,\n type,\n created\nFROM\n (\n SELECT\n author as actor,\n id as item,\n 'list' as type,\n created\n FROM\n lists\n WHERE lists.public = TRUE\n UNION\n SELECT\n author as actor,\n id as item,\n 'trail' as type,\n created\n FROM trails\n WHERE trails.public = TRUE\n )\nORDER BY created desc;"
|
||||
}`), &collection); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return app.Save(collection)
|
||||
}, func(app core.App) error {
|
||||
collection, err := app.FindCollectionByNameOrId("pbc_1973704172")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// update collection data
|
||||
if err := json.Unmarshal([]byte(`{
|
||||
"viewQuery": "SELECT\n (ROW_NUMBER() OVER ()) as id,\n actor,\n item,\n type,\n created\nFROM\n (\n SELECT\n author as actor,\n id as item,\n 'list' as type,\n created\n FROM\n lists\n UNION\n SELECT\n author as actor,\n id as item,\n 'trail' as type,\n created\n FROM trails\n )\nORDER BY created desc;"
|
||||
}`), &collection); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return app.Save(collection)
|
||||
})
|
||||
}
|
||||
42
db/migrations/1755690849_updated_activitypub_actors.go
Normal file
42
db/migrations/1755690849_updated_activitypub_actors.go
Normal file
@@ -0,0 +1,42 @@
|
||||
package migrations
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
m "github.com/pocketbase/pocketbase/migrations"
|
||||
)
|
||||
|
||||
func init() {
|
||||
m.Register(func(app core.App) error {
|
||||
collection, err := app.FindCollectionByNameOrId("pbc_1295301207")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// update collection data
|
||||
if err := json.Unmarshal([]byte(`{
|
||||
"listRule": "user = @request.auth.id",
|
||||
"viewRule": "user = @request.auth.id"
|
||||
}`), &collection); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return app.Save(collection)
|
||||
}, func(app core.App) error {
|
||||
collection, err := app.FindCollectionByNameOrId("pbc_1295301207")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// update collection data
|
||||
if err := json.Unmarshal([]byte(`{
|
||||
"listRule": "user.settings_via_user.privacy.account != 'private' || user = @request.auth.id",
|
||||
"viewRule": "user.settings_via_user.privacy.account != 'private' || user = @request.auth.id"
|
||||
}`), &collection); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return app.Save(collection)
|
||||
})
|
||||
}
|
||||
116
db/migrations/1755695750_created_trail_link_share.go
Normal file
116
db/migrations/1755695750_created_trail_link_share.go
Normal file
@@ -0,0 +1,116 @@
|
||||
package migrations
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
m "github.com/pocketbase/pocketbase/migrations"
|
||||
)
|
||||
|
||||
func init() {
|
||||
m.Register(func(app core.App) error {
|
||||
jsonData := `{
|
||||
"createRule": null,
|
||||
"deleteRule": null,
|
||||
"fields": [
|
||||
{
|
||||
"autogeneratePattern": "[a-z0-9]{15}",
|
||||
"hidden": false,
|
||||
"id": "text3208210256",
|
||||
"max": 15,
|
||||
"min": 15,
|
||||
"name": "id",
|
||||
"pattern": "^[a-z0-9]+$",
|
||||
"presentable": false,
|
||||
"primaryKey": true,
|
||||
"required": true,
|
||||
"system": true,
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"cascadeDelete": true,
|
||||
"collectionId": "e864strfxo14pm4",
|
||||
"hidden": false,
|
||||
"id": "relation2993194383",
|
||||
"maxSelect": 1,
|
||||
"minSelect": 0,
|
||||
"name": "trail",
|
||||
"presentable": false,
|
||||
"required": true,
|
||||
"system": false,
|
||||
"type": "relation"
|
||||
},
|
||||
{
|
||||
"autogeneratePattern": "[a-z0-9]{32}",
|
||||
"hidden": false,
|
||||
"id": "text1597481275",
|
||||
"max": 32,
|
||||
"min": 32,
|
||||
"name": "token",
|
||||
"pattern": "",
|
||||
"presentable": false,
|
||||
"primaryKey": false,
|
||||
"required": true,
|
||||
"system": false,
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"hidden": false,
|
||||
"id": "select3762918058",
|
||||
"maxSelect": 1,
|
||||
"name": "permission",
|
||||
"presentable": false,
|
||||
"required": true,
|
||||
"system": false,
|
||||
"type": "select",
|
||||
"values": [
|
||||
"view",
|
||||
"edit"
|
||||
]
|
||||
},
|
||||
{
|
||||
"hidden": false,
|
||||
"id": "autodate2990389176",
|
||||
"name": "created",
|
||||
"onCreate": true,
|
||||
"onUpdate": false,
|
||||
"presentable": false,
|
||||
"system": false,
|
||||
"type": "autodate"
|
||||
},
|
||||
{
|
||||
"hidden": false,
|
||||
"id": "autodate3332085495",
|
||||
"name": "updated",
|
||||
"onCreate": true,
|
||||
"onUpdate": true,
|
||||
"presentable": false,
|
||||
"system": false,
|
||||
"type": "autodate"
|
||||
}
|
||||
],
|
||||
"id": "pbc_2865004035",
|
||||
"indexes": [],
|
||||
"listRule": null,
|
||||
"name": "trail_link_share",
|
||||
"system": false,
|
||||
"type": "base",
|
||||
"updateRule": null,
|
||||
"viewRule": null
|
||||
}`
|
||||
|
||||
collection := &core.Collection{}
|
||||
if err := json.Unmarshal([]byte(jsonData), &collection); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return app.Save(collection)
|
||||
}, func(app core.App) error {
|
||||
collection, err := app.FindCollectionByNameOrId("pbc_2865004035")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return app.Delete(collection)
|
||||
})
|
||||
}
|
||||
42
db/migrations/1755696870_updated_trail_link_share.go
Normal file
42
db/migrations/1755696870_updated_trail_link_share.go
Normal file
@@ -0,0 +1,42 @@
|
||||
package migrations
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
m "github.com/pocketbase/pocketbase/migrations"
|
||||
)
|
||||
|
||||
func init() {
|
||||
m.Register(func(app core.App) error {
|
||||
collection, err := app.FindCollectionByNameOrId("pbc_2865004035")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// update collection data
|
||||
if err := json.Unmarshal([]byte(`{
|
||||
"indexes": [
|
||||
"CREATE UNIQUE INDEX `+"`"+`idx_YbuWvsh5la`+"`"+` ON `+"`"+`trail_link_share`+"`"+` (`+"`"+`trail`+"`"+`)"
|
||||
]
|
||||
}`), &collection); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return app.Save(collection)
|
||||
}, func(app core.App) error {
|
||||
collection, err := app.FindCollectionByNameOrId("pbc_2865004035")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// update collection data
|
||||
if err := json.Unmarshal([]byte(`{
|
||||
"indexes": []
|
||||
}`), &collection); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return app.Save(collection)
|
||||
})
|
||||
}
|
||||
48
db/migrations/1755697071_updated_trail_link_share.go
Normal file
48
db/migrations/1755697071_updated_trail_link_share.go
Normal file
@@ -0,0 +1,48 @@
|
||||
package migrations
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
m "github.com/pocketbase/pocketbase/migrations"
|
||||
)
|
||||
|
||||
func init() {
|
||||
m.Register(func(app core.App) error {
|
||||
collection, err := app.FindCollectionByNameOrId("pbc_2865004035")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// update collection data
|
||||
if err := json.Unmarshal([]byte(`{
|
||||
"createRule": "trail.author.user = @request.auth.id",
|
||||
"deleteRule": "trail.author.user = @request.auth.id",
|
||||
"listRule": "trail.author.user = @request.auth.id",
|
||||
"updateRule": "trail.author.user = @request.auth.id",
|
||||
"viewRule": "trail.author.user = @request.auth.id"
|
||||
}`), &collection); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return app.Save(collection)
|
||||
}, func(app core.App) error {
|
||||
collection, err := app.FindCollectionByNameOrId("pbc_2865004035")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// update collection data
|
||||
if err := json.Unmarshal([]byte(`{
|
||||
"createRule": null,
|
||||
"deleteRule": null,
|
||||
"listRule": null,
|
||||
"updateRule": null,
|
||||
"viewRule": null
|
||||
}`), &collection); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return app.Save(collection)
|
||||
})
|
||||
}
|
||||
42
db/migrations/1755697879_updated_trails.go
Normal file
42
db/migrations/1755697879_updated_trails.go
Normal file
@@ -0,0 +1,42 @@
|
||||
package migrations
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
m "github.com/pocketbase/pocketbase/migrations"
|
||||
)
|
||||
|
||||
func init() {
|
||||
m.Register(func(app core.App) error {
|
||||
collection, err := app.FindCollectionByNameOrId("e864strfxo14pm4")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// update collection data
|
||||
if err := json.Unmarshal([]byte(`{
|
||||
"listRule": "author.user = @request.auth.id || public = true || (@request.auth.id != \"\" && trail_share_via_trail.actor.user ?= @request.auth.id) || trail_link_share_via_trail.token = @request.query.share",
|
||||
"viewRule": "author.user = @request.auth.id || public = true || (@request.auth.id != \"\" && trail_share_via_trail.actor.user ?= @request.auth.id) || trail_link_share_via_trail.token = @request.query.share "
|
||||
}`), &collection); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return app.Save(collection)
|
||||
}, func(app core.App) error {
|
||||
collection, err := app.FindCollectionByNameOrId("e864strfxo14pm4")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// update collection data
|
||||
if err := json.Unmarshal([]byte(`{
|
||||
"listRule": "author.user = @request.auth.id || public = true || (@request.auth.id != \"\" && trail_share_via_trail.actor.user ?= @request.auth.id)",
|
||||
"viewRule": "author.user = @request.auth.id || public = true || (@request.auth.id != \"\" && trail_share_via_trail.actor.user ?= @request.auth.id)"
|
||||
}`), &collection); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return app.Save(collection)
|
||||
})
|
||||
}
|
||||
42
db/migrations/1755700187_updated_waypoints.go
Normal file
42
db/migrations/1755700187_updated_waypoints.go
Normal file
@@ -0,0 +1,42 @@
|
||||
package migrations
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
m "github.com/pocketbase/pocketbase/migrations"
|
||||
)
|
||||
|
||||
func init() {
|
||||
m.Register(func(app core.App) error {
|
||||
collection, err := app.FindCollectionByNameOrId("goeo2ubp103rzp9")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// update collection data
|
||||
if err := json.Unmarshal([]byte(`{
|
||||
"listRule": "author = @request.auth.id || trails_via_waypoints.author.user ?= @request.auth.id || trails_via_waypoints.public ?= true || \n(@collection.trail_share.trail.id ?= trails_via_waypoints.id && @collection.trail_share.actor.user ?= @request.auth.id)\n|| \n(@collection.trail_link_share.trail.id ?= trails_via_waypoints.id && @collection.trail_link_share.token = @request.query.share)",
|
||||
"viewRule": "author = @request.auth.id || trails_via_waypoints.author.user ?= @request.auth.id || trails_via_waypoints.public ?= true || \n(@collection.trail_share.trail.id ?= trails_via_waypoints.id && @collection.trail_share.actor.user ?= @request.auth.id)\n|| \n(@collection.trail_link_share.trail.id ?= trails_via_waypoints.id && @collection.trail_link_share.token = @request.query.share)"
|
||||
}`), &collection); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return app.Save(collection)
|
||||
}, func(app core.App) error {
|
||||
collection, err := app.FindCollectionByNameOrId("goeo2ubp103rzp9")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// update collection data
|
||||
if err := json.Unmarshal([]byte(`{
|
||||
"listRule": "author = @request.auth.id || trails_via_waypoints.author.user ?= @request.auth.id || trails_via_waypoints.public ?= true || \n(@collection.trail_share.trail.id ?= trails_via_waypoints.id && @collection.trail_share.actor.user ?= @request.auth.id)",
|
||||
"viewRule": "author = @request.auth.id || trails_via_waypoints.author.user ?= @request.auth.id || trails_via_waypoints.public ?= true || \n(@collection.trail_share.trail.id ?= trails_via_waypoints.id && @collection.trail_share.actor.user ?= @request.auth.id)"
|
||||
}`), &collection); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return app.Save(collection)
|
||||
})
|
||||
}
|
||||
42
db/migrations/1756040234_updated_activitypub_actors.go
Normal file
42
db/migrations/1756040234_updated_activitypub_actors.go
Normal file
@@ -0,0 +1,42 @@
|
||||
package migrations
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
m "github.com/pocketbase/pocketbase/migrations"
|
||||
)
|
||||
|
||||
func init() {
|
||||
m.Register(func(app core.App) error {
|
||||
collection, err := app.FindCollectionByNameOrId("pbc_1295301207")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// update collection data
|
||||
if err := json.Unmarshal([]byte(`{
|
||||
"listRule": "",
|
||||
"viewRule": ""
|
||||
}`), &collection); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return app.Save(collection)
|
||||
}, func(app core.App) error {
|
||||
collection, err := app.FindCollectionByNameOrId("pbc_1295301207")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// update collection data
|
||||
if err := json.Unmarshal([]byte(`{
|
||||
"listRule": "user = @request.auth.id",
|
||||
"viewRule": "user = @request.auth.id"
|
||||
}`), &collection); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return app.Save(collection)
|
||||
})
|
||||
}
|
||||
78
db/migrations/1756041217_updated_settings.go
Normal file
78
db/migrations/1756041217_updated_settings.go
Normal file
@@ -0,0 +1,78 @@
|
||||
package migrations
|
||||
|
||||
import (
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
m "github.com/pocketbase/pocketbase/migrations"
|
||||
)
|
||||
|
||||
func init() {
|
||||
m.Register(func(app core.App) error {
|
||||
collection, err := app.FindCollectionByNameOrId("uavt73rsqcn1n13")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// update field
|
||||
if err := collection.Fields.AddMarshaledJSONAt(1, []byte(`{
|
||||
"hidden": false,
|
||||
"id": "0sepzvkh",
|
||||
"maxSelect": 1,
|
||||
"name": "language",
|
||||
"presentable": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "select",
|
||||
"values": [
|
||||
"en",
|
||||
"de",
|
||||
"fr",
|
||||
"hu",
|
||||
"it",
|
||||
"nl",
|
||||
"pl",
|
||||
"pt",
|
||||
"zh",
|
||||
"es",
|
||||
"eu",
|
||||
"ru"
|
||||
]
|
||||
}`)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return app.Save(collection)
|
||||
}, func(app core.App) error {
|
||||
collection, err := app.FindCollectionByNameOrId("uavt73rsqcn1n13")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// update field
|
||||
if err := collection.Fields.AddMarshaledJSONAt(1, []byte(`{
|
||||
"hidden": false,
|
||||
"id": "0sepzvkh",
|
||||
"maxSelect": 1,
|
||||
"name": "language",
|
||||
"presentable": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "select",
|
||||
"values": [
|
||||
"en",
|
||||
"de",
|
||||
"fr",
|
||||
"hu",
|
||||
"it",
|
||||
"nl",
|
||||
"pl",
|
||||
"pt",
|
||||
"zh",
|
||||
"es"
|
||||
]
|
||||
}`)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return app.Save(collection)
|
||||
})
|
||||
}
|
||||
42
db/migrations/1756471290_updated_trails.go
Normal file
42
db/migrations/1756471290_updated_trails.go
Normal file
@@ -0,0 +1,42 @@
|
||||
package migrations
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
m "github.com/pocketbase/pocketbase/migrations"
|
||||
)
|
||||
|
||||
func init() {
|
||||
m.Register(func(app core.App) error {
|
||||
collection, err := app.FindCollectionByNameOrId("e864strfxo14pm4")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// update collection data
|
||||
if err := json.Unmarshal([]byte(`{
|
||||
"listRule": "author.user = @request.auth.id || public = true || (@request.auth.id != \"\" && trail_share_via_trail.actor.user ?= @request.auth.id) || (trail_link_share_via_trail.token != \"\" && trail_link_share_via_trail.token = @request.query.share)",
|
||||
"viewRule": "author.user = @request.auth.id || public = true || (@request.auth.id != \"\" && trail_share_via_trail.actor.user ?= @request.auth.id) || (trail_link_share_via_trail.token != \"\" && trail_link_share_via_trail.token = @request.query.share)"
|
||||
}`), &collection); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return app.Save(collection)
|
||||
}, func(app core.App) error {
|
||||
collection, err := app.FindCollectionByNameOrId("e864strfxo14pm4")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// update collection data
|
||||
if err := json.Unmarshal([]byte(`{
|
||||
"listRule": "author.user = @request.auth.id || public = true || (@request.auth.id != \"\" && trail_share_via_trail.actor.user ?= @request.auth.id) || trail_link_share_via_trail.token = @request.query.share",
|
||||
"viewRule": "author.user = @request.auth.id || public = true || (@request.auth.id != \"\" && trail_share_via_trail.actor.user ?= @request.auth.id) || trail_link_share_via_trail.token = @request.query.share "
|
||||
}`), &collection); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return app.Save(collection)
|
||||
})
|
||||
}
|
||||
42
db/migrations/1756473199_updated_waypoints.go
Normal file
42
db/migrations/1756473199_updated_waypoints.go
Normal file
@@ -0,0 +1,42 @@
|
||||
package migrations
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
m "github.com/pocketbase/pocketbase/migrations"
|
||||
)
|
||||
|
||||
func init() {
|
||||
m.Register(func(app core.App) error {
|
||||
collection, err := app.FindCollectionByNameOrId("goeo2ubp103rzp9")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// update collection data
|
||||
if err := json.Unmarshal([]byte(`{
|
||||
"listRule": "author = @request.auth.id || trails_via_waypoints.author.user ?= @request.auth.id || trails_via_waypoints.public ?= true || \n(@collection.trail_share.trail.id ?= trails_via_waypoints.id && @collection.trail_share.actor.user ?= @request.auth.id)\n|| \n(@collection.trail_link_share.token != \"\" && @collection.trail_link_share.trail.waypoints.id ?= id)",
|
||||
"viewRule": "author = @request.auth.id || trails_via_waypoints.author.user ?= @request.auth.id || trails_via_waypoints.public ?= true || \n(@collection.trail_share.trail.id ?= trails_via_waypoints.id && @collection.trail_share.actor.user ?= @request.auth.id)\n|| \n(@collection.trail_link_share.token != \"\" && @collection.trail_link_share.trail.waypoints.id ?= id)"
|
||||
}`), &collection); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return app.Save(collection)
|
||||
}, func(app core.App) error {
|
||||
collection, err := app.FindCollectionByNameOrId("goeo2ubp103rzp9")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// update collection data
|
||||
if err := json.Unmarshal([]byte(`{
|
||||
"listRule": "author = @request.auth.id || trails_via_waypoints.author.user ?= @request.auth.id || trails_via_waypoints.public ?= true || \n(@collection.trail_share.trail.id ?= trails_via_waypoints.id && @collection.trail_share.actor.user ?= @request.auth.id)\n|| \n(@collection.trail_link_share.trail.id ?= trails_via_waypoints.id && @collection.trail_link_share.token = @request.query.share)",
|
||||
"viewRule": "author = @request.auth.id || trails_via_waypoints.author.user ?= @request.auth.id || trails_via_waypoints.public ?= true || \n(@collection.trail_share.trail.id ?= trails_via_waypoints.id && @collection.trail_share.actor.user ?= @request.auth.id)\n|| \n(@collection.trail_link_share.trail.id ?= trails_via_waypoints.id && @collection.trail_link_share.token = @request.query.share)"
|
||||
}`), &collection); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return app.Save(collection)
|
||||
})
|
||||
}
|
||||
62
db/migrations/1757689107_updated_waypoints.go
Normal file
62
db/migrations/1757689107_updated_waypoints.go
Normal file
@@ -0,0 +1,62 @@
|
||||
package migrations
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
m "github.com/pocketbase/pocketbase/migrations"
|
||||
)
|
||||
|
||||
func init() {
|
||||
m.Register(func(app core.App) error {
|
||||
collection, err := app.FindCollectionByNameOrId("goeo2ubp103rzp9")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// update collection data
|
||||
if err := json.Unmarshal([]byte(`{
|
||||
"listRule": "author = @request.auth.id || trail.author.user ?= @request.auth.id || trail.public ?= true || trail.trail_share_via_trail.actor.user ?= @request.auth.id\n|| \n(trail.trail_link_share_via_trail.token != \"\" && trail.trail_link_share_via_trail.token = @request.query.share)",
|
||||
"viewRule": "author = @request.auth.id || trail.author.user ?= @request.auth.id || trail.public ?= true || trail.trail_share_via_trail.actor.user ?= @request.auth.id\n|| \n(trail.trail_link_share_via_trail.token != \"\" && trail.trail_link_share_via_trail.token = @request.query.share)"
|
||||
}`), &collection); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// add field
|
||||
if err := collection.Fields.AddMarshaledJSONAt(9, []byte(`{
|
||||
"cascadeDelete": true,
|
||||
"collectionId": "e864strfxo14pm4",
|
||||
"hidden": false,
|
||||
"id": "relation2993194383",
|
||||
"maxSelect": 1,
|
||||
"minSelect": 0,
|
||||
"name": "trail",
|
||||
"presentable": false,
|
||||
"required": true,
|
||||
"system": false,
|
||||
"type": "relation"
|
||||
}`)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return app.Save(collection)
|
||||
}, func(app core.App) error {
|
||||
collection, err := app.FindCollectionByNameOrId("goeo2ubp103rzp9")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// update collection data
|
||||
if err := json.Unmarshal([]byte(`{
|
||||
"listRule": "author = @request.auth.id || trails_via_waypoints.author.user ?= @request.auth.id || trails_via_waypoints.public ?= true || \n(@collection.trail_share.trail.id ?= trails_via_waypoints.id && @collection.trail_share.actor.user ?= @request.auth.id)\n|| \n(@collection.trail_link_share.token != \"\" && @collection.trail_link_share.trail.waypoints.id ?= id)",
|
||||
"viewRule": "author = @request.auth.id || trails_via_waypoints.author.user ?= @request.auth.id || trails_via_waypoints.public ?= true || \n(@collection.trail_share.trail.id ?= trails_via_waypoints.id && @collection.trail_share.actor.user ?= @request.auth.id)\n|| \n(@collection.trail_link_share.token != \"\" && @collection.trail_link_share.trail.waypoints.id ?= id)"
|
||||
}`), &collection); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// remove field
|
||||
collection.Fields.RemoveById("relation2993194383")
|
||||
|
||||
return app.Save(collection)
|
||||
})
|
||||
}
|
||||
34
db/migrations/1757689185_migrate_waypoint_trails.go
Normal file
34
db/migrations/1757689185_migrate_waypoint_trails.go
Normal file
@@ -0,0 +1,34 @@
|
||||
package migrations
|
||||
|
||||
import (
|
||||
"github.com/pocketbase/dbx"
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
m "github.com/pocketbase/pocketbase/migrations"
|
||||
)
|
||||
|
||||
func init() {
|
||||
m.Register(func(app core.App) error {
|
||||
wps, err := app.FindAllRecords("waypoints")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, wp := range wps {
|
||||
trail, err := app.FindFirstRecordByFilter("trails", "waypoints ?~ {:id}", dbx.Params{"id": wp.Id})
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
wp.Set("trail", trail.Id)
|
||||
err = app.UnsafeWithoutHooks().Save(wp)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}, func(app core.App) error {
|
||||
// add down queries...
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
44
db/migrations/1757690946_updated_trails.go
Normal file
44
db/migrations/1757690946_updated_trails.go
Normal file
@@ -0,0 +1,44 @@
|
||||
package migrations
|
||||
|
||||
import (
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
m "github.com/pocketbase/pocketbase/migrations"
|
||||
)
|
||||
|
||||
func init() {
|
||||
m.Register(func(app core.App) error {
|
||||
collection, err := app.FindCollectionByNameOrId("e864strfxo14pm4")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// remove field
|
||||
collection.Fields.RemoveById("ppq2sist")
|
||||
|
||||
return app.Save(collection)
|
||||
}, func(app core.App) error {
|
||||
collection, err := app.FindCollectionByNameOrId("e864strfxo14pm4")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// add field
|
||||
if err := collection.Fields.AddMarshaledJSONAt(16, []byte(`{
|
||||
"cascadeDelete": false,
|
||||
"collectionId": "goeo2ubp103rzp9",
|
||||
"hidden": false,
|
||||
"id": "ppq2sist",
|
||||
"maxSelect": 2147483647,
|
||||
"minSelect": 0,
|
||||
"name": "waypoints",
|
||||
"presentable": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "relation"
|
||||
}`)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return app.Save(collection)
|
||||
})
|
||||
}
|
||||
46
db/migrations/1757759342_updated_activitypub_actors.go
Normal file
46
db/migrations/1757759342_updated_activitypub_actors.go
Normal file
@@ -0,0 +1,46 @@
|
||||
package migrations
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
m "github.com/pocketbase/pocketbase/migrations"
|
||||
)
|
||||
|
||||
func init() {
|
||||
m.Register(func(app core.App) error {
|
||||
collection, err := app.FindCollectionByNameOrId("pbc_1295301207")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// update collection data
|
||||
if err := json.Unmarshal([]byte(`{
|
||||
"indexes": [
|
||||
"CREATE UNIQUE INDEX `+"`"+`idx_rpT7QJwWTm`+"`"+` ON `+"`"+`activitypub_actors`+"`"+` (`+"`"+`iri`+"`"+`)",
|
||||
"CREATE INDEX idx_actors_username_domain\nON activitypub_actors(preferred_username, domain);",
|
||||
"CREATE INDEX idx_activitypub_actors_user ON activitypub_actors(user);"
|
||||
]
|
||||
}`), &collection); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return app.Save(collection)
|
||||
}, func(app core.App) error {
|
||||
collection, err := app.FindCollectionByNameOrId("pbc_1295301207")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// update collection data
|
||||
if err := json.Unmarshal([]byte(`{
|
||||
"indexes": [
|
||||
"CREATE UNIQUE INDEX `+"`"+`idx_rpT7QJwWTm`+"`"+` ON `+"`"+`activitypub_actors`+"`"+` (`+"`"+`iri`+"`"+`)"
|
||||
]
|
||||
}`), &collection); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return app.Save(collection)
|
||||
})
|
||||
}
|
||||
42
db/migrations/1758283285_updated_waypoints.go
Normal file
42
db/migrations/1758283285_updated_waypoints.go
Normal file
@@ -0,0 +1,42 @@
|
||||
package migrations
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
m "github.com/pocketbase/pocketbase/migrations"
|
||||
)
|
||||
|
||||
func init() {
|
||||
m.Register(func(app core.App) error {
|
||||
collection, err := app.FindCollectionByNameOrId("goeo2ubp103rzp9")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// update collection data
|
||||
if err := json.Unmarshal([]byte(`{
|
||||
"deleteRule": "@request.auth.id != \"\" && (trail.author.user = @request.auth.id || author = @request.auth.id)",
|
||||
"updateRule": "@request.auth.id != \"\" && (trail.author.user = @request.auth.id || author = @request.auth.id)"
|
||||
}`), &collection); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return app.Save(collection)
|
||||
}, func(app core.App) error {
|
||||
collection, err := app.FindCollectionByNameOrId("goeo2ubp103rzp9")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// update collection data
|
||||
if err := json.Unmarshal([]byte(`{
|
||||
"deleteRule": "@request.auth.id != \"\" && ((@collection.trails.waypoints.id ?= id && @collection.trails.author.user = @request.auth.id) || author = @request.auth.id)",
|
||||
"updateRule": "@request.auth.id != \"\" && ((@collection.trails.waypoints.id ?= id && @collection.trails.author.user = @request.auth.id) || author = @request.auth.id)"
|
||||
}`), &collection); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return app.Save(collection)
|
||||
})
|
||||
}
|
||||
41
db/migrations/1758392924_updated_settings.go
Normal file
41
db/migrations/1758392924_updated_settings.go
Normal file
@@ -0,0 +1,41 @@
|
||||
package migrations
|
||||
|
||||
import (
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
m "github.com/pocketbase/pocketbase/migrations"
|
||||
)
|
||||
|
||||
func init() {
|
||||
m.Register(func(app core.App) error {
|
||||
collection, err := app.FindCollectionByNameOrId("uavt73rsqcn1n13")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// add field
|
||||
if err := collection.Fields.AddMarshaledJSONAt(12, []byte(`{
|
||||
"hidden": false,
|
||||
"id": "json1001103536",
|
||||
"maxSize": 0,
|
||||
"name": "behavior",
|
||||
"presentable": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "json"
|
||||
}`)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return app.Save(collection)
|
||||
}, func(app core.App) error {
|
||||
collection, err := app.FindCollectionByNameOrId("uavt73rsqcn1n13")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// remove field
|
||||
collection.Fields.RemoveById("json1001103536")
|
||||
|
||||
return app.Save(collection)
|
||||
})
|
||||
}
|
||||
41
db/migrations/1760706161_updated_integrations.go
Normal file
41
db/migrations/1760706161_updated_integrations.go
Normal file
@@ -0,0 +1,41 @@
|
||||
package migrations
|
||||
|
||||
import (
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
m "github.com/pocketbase/pocketbase/migrations"
|
||||
)
|
||||
|
||||
func init() {
|
||||
m.Register(func(app core.App) error {
|
||||
collection, err := app.FindCollectionByNameOrId("iz4sezoehde64wp")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// add field
|
||||
if err := collection.Fields.AddMarshaledJSONAt(4, []byte(`{
|
||||
"hidden": false,
|
||||
"id": "json2528191900",
|
||||
"maxSize": 2000000,
|
||||
"name": "hammerhead",
|
||||
"presentable": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "json"
|
||||
}`)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return app.Save(collection)
|
||||
}, func(app core.App) error {
|
||||
collection, err := app.FindCollectionByNameOrId("iz4sezoehde64wp")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// remove field
|
||||
collection.Fields.RemoveById("json2528191900")
|
||||
|
||||
return app.Save(collection)
|
||||
})
|
||||
}
|
||||
61
db/migrations/1760715417_updated_trails.go
Normal file
61
db/migrations/1760715417_updated_trails.go
Normal file
@@ -0,0 +1,61 @@
|
||||
package migrations
|
||||
|
||||
import (
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
m "github.com/pocketbase/pocketbase/migrations"
|
||||
)
|
||||
|
||||
func init() {
|
||||
m.Register(func(app core.App) error {
|
||||
collection, err := app.FindCollectionByNameOrId("e864strfxo14pm4")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// update field
|
||||
if err := collection.Fields.AddMarshaledJSONAt(20, []byte(`{
|
||||
"hidden": false,
|
||||
"id": "htr35nha",
|
||||
"maxSelect": 1,
|
||||
"name": "external_provider",
|
||||
"presentable": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "select",
|
||||
"values": [
|
||||
"strava",
|
||||
"komoot",
|
||||
"hammerhead"
|
||||
]
|
||||
}`)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return app.Save(collection)
|
||||
}, func(app core.App) error {
|
||||
collection, err := app.FindCollectionByNameOrId("e864strfxo14pm4")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// update field
|
||||
if err := collection.Fields.AddMarshaledJSONAt(20, []byte(`{
|
||||
"hidden": false,
|
||||
"id": "htr35nha",
|
||||
"maxSelect": 1,
|
||||
"name": "external_provider",
|
||||
"presentable": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "select",
|
||||
"values": [
|
||||
"strava",
|
||||
"komoot"
|
||||
]
|
||||
}`)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return app.Save(collection)
|
||||
})
|
||||
}
|
||||
60
db/migrations/1763300311_updated_categories.go
Normal file
60
db/migrations/1763300311_updated_categories.go
Normal file
@@ -0,0 +1,60 @@
|
||||
package migrations
|
||||
|
||||
import (
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
m "github.com/pocketbase/pocketbase/migrations"
|
||||
)
|
||||
|
||||
func init() {
|
||||
m.Register(func(app core.App) error {
|
||||
collection, err := app.FindCollectionByNameOrId("kjxvi8asj2igqwf")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// add field
|
||||
if err := collection.Fields.AddMarshaledJSONAt(6, []byte(`{
|
||||
"hidden": false,
|
||||
"id": "json3846545605",
|
||||
"maxSize": 0,
|
||||
"name": "settings",
|
||||
"presentable": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "json"
|
||||
}`)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := app.Save(collection); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
records, err := app.FindAllRecords("categories")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, record := range records {
|
||||
record.Set("settings", map[string]any{
|
||||
"wp_merge_enabled": true,
|
||||
"wp_merge_radius": 50,
|
||||
})
|
||||
if err := app.Save(record); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}, func(app core.App) error {
|
||||
collection, err := app.FindCollectionByNameOrId("kjxvi8asj2igqwf")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// remove field
|
||||
collection.Fields.RemoveById("json3846545605")
|
||||
|
||||
return app.Save(collection)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package migrations
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
m "github.com/pocketbase/pocketbase/migrations"
|
||||
)
|
||||
|
||||
func init() {
|
||||
m.Register(func(app core.App) error {
|
||||
collection, err := app.FindCollectionByNameOrId("urytyc428mwlbqq")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := json.Unmarshal([]byte(`{
|
||||
"viewQuery": "SELECT \n a.id, a.user, \n COALESCE(MAX(t.max_lat), 0) AS max_lat, \n COALESCE(MAX(t.max_lon), 0) AS max_lon, \n COALESCE(MIN(t.min_lat), 0) AS min_lat, \n COALESCE(MIN(t.min_lon), 0) AS min_lon \nFROM activitypub_actors a \nLEFT JOIN ( \n SELECT author AS actor_id, \n MAX(lat) AS max_lat, \n MAX(lon) AS max_lon, \n MIN(lat) AS min_lat, \n MIN(lon) AS min_lon \n FROM trails \n GROUP BY author \n UNION ALL \n SELECT ts.actor AS actor_id, \n MAX(t.lat) AS max_lat, \n MAX(t.lon) AS max_lon, \n MIN(t.lat) AS min_lat, \n MIN(t.lon) AS min_lon \n FROM trail_share ts \n JOIN trails t ON t.id = ts.trail \n GROUP BY ts.actor \n UNION ALL \n SELECT a2.id AS actor_id, \n p.max_lat, p.max_lon, p.min_lat, p.min_lon \n FROM activitypub_actors a2 \n CROSS JOIN ( \n SELECT \n MAX(lat) AS max_lat, \n MAX(lon) AS max_lon, \n MIN(lat) AS min_lat, \n MIN(lon) AS min_lon \n FROM trails \n WHERE public = TRUE \n ) p \n) t ON t.actor_id = a.id \nGROUP BY a.id;"
|
||||
}`), &collection); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return app.Save(collection)
|
||||
}, func(app core.App) error {
|
||||
collection, err := app.FindCollectionByNameOrId("urytyc428mwlbqq")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := json.Unmarshal([]byte(`{
|
||||
"viewQuery": "SELECT \n activitypub_actors.id, activitypub_actors.user, \n COALESCE(MAX(trails.lat), 0) AS max_lat, \n COALESCE(MAX(trails.lon), 0) AS max_lon, \n COALESCE(MIN(trails.lat), 0) AS min_lat, \n COALESCE(MIN(trails.lon), 0) AS min_lon \nFROM activitypub_actors \nLEFT JOIN trails \n ON activitypub_actors.id = trails.author \n OR trails.public = TRUE \n OR EXISTS (\n SELECT 1 \n FROM trail_share \n WHERE trail_share.trail = trails.id \n AND trail_share.actor = activitypub_actors.id\n ) \nGROUP BY activitypub_actors.id;"
|
||||
}`), &collection); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return app.Save(collection)
|
||||
})
|
||||
}
|
||||
120
db/migrations/1770000002_optimize_trails_filter_view.go
Normal file
120
db/migrations/1770000002_optimize_trails_filter_view.go
Normal file
@@ -0,0 +1,120 @@
|
||||
package migrations
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
m "github.com/pocketbase/pocketbase/migrations"
|
||||
)
|
||||
|
||||
func init() {
|
||||
m.Register(func(app core.App) error {
|
||||
collection, err := app.FindCollectionByNameOrId("trails_filter")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
viewQuery := `SELECT
|
||||
a.id,
|
||||
a.user,
|
||||
COALESCE(printf("%.2f", MAX(t.max_distance)), 0) AS max_distance,
|
||||
COALESCE(printf("%.2f", MAX(t.max_elevation_gain)), 0) AS max_elevation_gain,
|
||||
COALESCE(printf("%.2f", MAX(t.max_elevation_loss)), 0) AS max_elevation_loss,
|
||||
COALESCE(printf("%.2f", MAX(t.max_duration)), 0) AS max_duration,
|
||||
COALESCE(printf("%.2f", MIN(t.min_distance)), 0) AS min_distance,
|
||||
COALESCE(printf("%.2f", MIN(t.min_elevation_gain)), 0) AS min_elevation_gain,
|
||||
COALESCE(printf("%.2f", MIN(t.min_elevation_loss)), 0) AS min_elevation_loss,
|
||||
COALESCE(printf("%.2f", MIN(t.min_duration)), 0) AS min_duration
|
||||
FROM activitypub_actors a
|
||||
LEFT JOIN (
|
||||
SELECT author AS actor_id,
|
||||
MAX(distance) AS max_distance,
|
||||
MIN(distance) AS min_distance,
|
||||
MAX(elevation_gain) AS max_elevation_gain,
|
||||
MIN(elevation_gain) AS min_elevation_gain,
|
||||
MAX(elevation_loss) AS max_elevation_loss,
|
||||
MIN(elevation_loss) AS min_elevation_loss,
|
||||
MAX(duration) AS max_duration,
|
||||
MIN(duration) AS min_duration
|
||||
FROM trails
|
||||
GROUP BY author
|
||||
UNION ALL
|
||||
SELECT ts.actor AS actor_id,
|
||||
MAX(t.distance) AS max_distance,
|
||||
MIN(t.distance) AS min_distance,
|
||||
MAX(t.elevation_gain) AS max_elevation_gain,
|
||||
MIN(t.elevation_gain) AS min_elevation_gain,
|
||||
MAX(t.elevation_loss) AS max_elevation_loss,
|
||||
MIN(t.elevation_loss) AS min_elevation_loss,
|
||||
MAX(t.duration) AS max_duration,
|
||||
MIN(t.duration) AS min_duration
|
||||
FROM trail_share ts
|
||||
JOIN trails t ON t.id = ts.trail
|
||||
GROUP BY ts.actor
|
||||
UNION ALL
|
||||
SELECT a2.id AS actor_id,
|
||||
p.max_distance,
|
||||
p.min_distance,
|
||||
p.max_elevation_gain,
|
||||
p.min_elevation_gain,
|
||||
p.max_elevation_loss,
|
||||
p.min_elevation_loss,
|
||||
p.max_duration,
|
||||
p.min_duration
|
||||
FROM activitypub_actors a2
|
||||
CROSS JOIN (
|
||||
SELECT
|
||||
MAX(distance) AS max_distance,
|
||||
MIN(distance) AS min_distance,
|
||||
MAX(elevation_gain) AS max_elevation_gain,
|
||||
MIN(elevation_gain) AS min_elevation_gain,
|
||||
MAX(elevation_loss) AS max_elevation_loss,
|
||||
MIN(elevation_loss) AS min_elevation_loss,
|
||||
MAX(duration) AS max_duration,
|
||||
MIN(duration) AS min_duration
|
||||
FROM trails
|
||||
WHERE public = TRUE
|
||||
) p
|
||||
) t ON t.actor_id = a.id
|
||||
GROUP BY a.id;`
|
||||
|
||||
if err := json.Unmarshal([]byte(`{"viewQuery": `+jsonMarshal(viewQuery)+`}`), &collection); err != nil {
|
||||
return err
|
||||
}
|
||||
return app.Save(collection)
|
||||
}, func(app core.App) error {
|
||||
collection, err := app.FindCollectionByNameOrId("trails_filter")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
viewQuery := `SELECT activitypub_actors.id, activitypub_actors.user, COALESCE(printf("%.2f", MAX(trails.distance)), 0) AS max_distance,
|
||||
COALESCE(printf("%.2f", MAX(trails.elevation_gain)), 0) AS max_elevation_gain,
|
||||
COALESCE(printf("%.2f", MAX(trails.elevation_loss)), 0) AS max_elevation_loss,
|
||||
COALESCE(printf("%.2f", MAX(trails.duration)), 0) AS max_duration,
|
||||
COALESCE(printf("%.2f", MIN(trails.distance)), 0) AS min_distance,
|
||||
COALESCE(printf("%.2f", MIN(trails.elevation_gain)), 0) AS min_elevation_gain,
|
||||
COALESCE(printf("%.2f", MIN(trails.elevation_loss)), 0) AS min_elevation_loss,
|
||||
COALESCE(printf("%.2f", MIN(trails.duration)), 0) AS min_duration
|
||||
FROM activitypub_actors
|
||||
LEFT JOIN trails ON
|
||||
activitypub_actors.id = trails.author OR
|
||||
trails.public = 1 OR
|
||||
EXISTS (
|
||||
SELECT 1
|
||||
FROM trail_share
|
||||
WHERE trail_share.trail = trails.id
|
||||
AND trail_share.actor = activitypub_actors.id
|
||||
) GROUP BY activitypub_actors.id;`
|
||||
|
||||
if err := json.Unmarshal([]byte(`{"viewQuery": `+jsonMarshal(viewQuery)+`}`), &collection); err != nil {
|
||||
return err
|
||||
}
|
||||
return app.Save(collection)
|
||||
})
|
||||
}
|
||||
|
||||
func jsonMarshal(value string) string {
|
||||
b, _ := json.Marshal(value)
|
||||
return string(b)
|
||||
}
|
||||
81
db/migrations/1770043888_updated_settings.go
Normal file
81
db/migrations/1770043888_updated_settings.go
Normal file
@@ -0,0 +1,81 @@
|
||||
package migrations
|
||||
|
||||
import (
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
m "github.com/pocketbase/pocketbase/migrations"
|
||||
)
|
||||
|
||||
func init() {
|
||||
m.Register(func(app core.App) error {
|
||||
collection, err := app.FindCollectionByNameOrId("uavt73rsqcn1n13")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// update field
|
||||
if err := collection.Fields.AddMarshaledJSONAt(1, []byte(`{
|
||||
"hidden": false,
|
||||
"id": "0sepzvkh",
|
||||
"maxSelect": 1,
|
||||
"name": "language",
|
||||
"presentable": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "select",
|
||||
"values": [
|
||||
"en",
|
||||
"de",
|
||||
"fr",
|
||||
"hu",
|
||||
"it",
|
||||
"nl",
|
||||
"pl",
|
||||
"pt",
|
||||
"zh",
|
||||
"es",
|
||||
"eu",
|
||||
"ru",
|
||||
"no"
|
||||
]
|
||||
}`)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return app.Save(collection)
|
||||
}, func(app core.App) error {
|
||||
collection, err := app.FindCollectionByNameOrId("uavt73rsqcn1n13")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// update field
|
||||
if err := collection.Fields.AddMarshaledJSONAt(1, []byte(`{
|
||||
"hidden": false,
|
||||
"id": "0sepzvkh",
|
||||
"maxSelect": 1,
|
||||
"name": "language",
|
||||
"presentable": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "select",
|
||||
"values": [
|
||||
"en",
|
||||
"de",
|
||||
"fr",
|
||||
"hu",
|
||||
"it",
|
||||
"nl",
|
||||
"pl",
|
||||
"pt",
|
||||
"zh",
|
||||
"es",
|
||||
"eu",
|
||||
"ru"
|
||||
]
|
||||
}`)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return app.Save(collection)
|
||||
})
|
||||
}
|
||||
83
db/migrations/1770279739_updated_settings.go
Normal file
83
db/migrations/1770279739_updated_settings.go
Normal file
@@ -0,0 +1,83 @@
|
||||
package migrations
|
||||
|
||||
import (
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
m "github.com/pocketbase/pocketbase/migrations"
|
||||
)
|
||||
|
||||
func init() {
|
||||
m.Register(func(app core.App) error {
|
||||
collection, err := app.FindCollectionByNameOrId("uavt73rsqcn1n13")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// update field
|
||||
if err := collection.Fields.AddMarshaledJSONAt(1, []byte(`{
|
||||
"hidden": false,
|
||||
"id": "0sepzvkh",
|
||||
"maxSelect": 1,
|
||||
"name": "language",
|
||||
"presentable": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "select",
|
||||
"values": [
|
||||
"en",
|
||||
"de",
|
||||
"fr",
|
||||
"hu",
|
||||
"it",
|
||||
"nl",
|
||||
"pl",
|
||||
"pt",
|
||||
"zh",
|
||||
"es",
|
||||
"eu",
|
||||
"ru",
|
||||
"no",
|
||||
"cs"
|
||||
]
|
||||
}`)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return app.Save(collection)
|
||||
}, func(app core.App) error {
|
||||
collection, err := app.FindCollectionByNameOrId("uavt73rsqcn1n13")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// update field
|
||||
if err := collection.Fields.AddMarshaledJSONAt(1, []byte(`{
|
||||
"hidden": false,
|
||||
"id": "0sepzvkh",
|
||||
"maxSelect": 1,
|
||||
"name": "language",
|
||||
"presentable": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "select",
|
||||
"values": [
|
||||
"en",
|
||||
"de",
|
||||
"fr",
|
||||
"hu",
|
||||
"it",
|
||||
"nl",
|
||||
"pl",
|
||||
"pt",
|
||||
"zh",
|
||||
"es",
|
||||
"eu",
|
||||
"ru",
|
||||
"no"
|
||||
]
|
||||
}`)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return app.Save(collection)
|
||||
})
|
||||
}
|
||||
45
db/migrations/1772036961_updated_users.go
Normal file
45
db/migrations/1772036961_updated_users.go
Normal file
@@ -0,0 +1,45 @@
|
||||
package migrations
|
||||
|
||||
import (
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
m "github.com/pocketbase/pocketbase/migrations"
|
||||
)
|
||||
|
||||
func init() {
|
||||
m.Register(func(app core.App) error {
|
||||
collection, err := app.FindCollectionByNameOrId("_pb_users_auth_")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// remove field
|
||||
collection.Fields.RemoveById("dlzhxcn2")
|
||||
|
||||
return app.Save(collection)
|
||||
}, func(app core.App) error {
|
||||
collection, err := app.FindCollectionByNameOrId("_pb_users_auth_")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// add field
|
||||
if err := collection.Fields.AddMarshaledJSONAt(7, []byte(`{
|
||||
"autogeneratePattern": "",
|
||||
"hidden": false,
|
||||
"id": "dlzhxcn2",
|
||||
"max": 0,
|
||||
"min": 0,
|
||||
"name": "token",
|
||||
"pattern": "",
|
||||
"presentable": false,
|
||||
"primaryKey": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "text"
|
||||
}`)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return app.Save(collection)
|
||||
})
|
||||
}
|
||||
138
db/migrations/1772293922_created_api_tokens.go
Normal file
138
db/migrations/1772293922_created_api_tokens.go
Normal file
@@ -0,0 +1,138 @@
|
||||
package migrations
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
m "github.com/pocketbase/pocketbase/migrations"
|
||||
)
|
||||
|
||||
func init() {
|
||||
m.Register(func(app core.App) error {
|
||||
jsonData := `{
|
||||
"createRule": "user = @request.auth.id",
|
||||
"deleteRule": "user = @request.auth.id",
|
||||
"fields": [
|
||||
{
|
||||
"autogeneratePattern": "[a-z0-9]{15}",
|
||||
"hidden": false,
|
||||
"id": "text3208210256",
|
||||
"max": 15,
|
||||
"min": 15,
|
||||
"name": "id",
|
||||
"pattern": "^[a-z0-9]+$",
|
||||
"presentable": false,
|
||||
"primaryKey": true,
|
||||
"required": true,
|
||||
"system": true,
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"autogeneratePattern": "",
|
||||
"hidden": false,
|
||||
"id": "text1579384326",
|
||||
"max": 0,
|
||||
"min": 0,
|
||||
"name": "name",
|
||||
"pattern": "",
|
||||
"presentable": false,
|
||||
"primaryKey": false,
|
||||
"required": true,
|
||||
"system": false,
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"autogeneratePattern": "",
|
||||
"hidden": true,
|
||||
"id": "text1597481275",
|
||||
"max": 64,
|
||||
"min": 64,
|
||||
"name": "token",
|
||||
"pattern": "",
|
||||
"presentable": false,
|
||||
"primaryKey": false,
|
||||
"required": true,
|
||||
"system": false,
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"hidden": false,
|
||||
"id": "date617435213",
|
||||
"max": "",
|
||||
"min": "",
|
||||
"name": "expiration",
|
||||
"presentable": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "date"
|
||||
},
|
||||
{
|
||||
"hidden": false,
|
||||
"id": "date4016875332",
|
||||
"max": "",
|
||||
"min": "",
|
||||
"name": "last_used",
|
||||
"presentable": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "date"
|
||||
},
|
||||
{
|
||||
"cascadeDelete": false,
|
||||
"collectionId": "_pb_users_auth_",
|
||||
"hidden": false,
|
||||
"id": "relation2375276105",
|
||||
"maxSelect": 1,
|
||||
"minSelect": 0,
|
||||
"name": "user",
|
||||
"presentable": false,
|
||||
"required": true,
|
||||
"system": false,
|
||||
"type": "relation"
|
||||
},
|
||||
{
|
||||
"hidden": false,
|
||||
"id": "autodate2990389176",
|
||||
"name": "created",
|
||||
"onCreate": true,
|
||||
"onUpdate": false,
|
||||
"presentable": false,
|
||||
"system": false,
|
||||
"type": "autodate"
|
||||
},
|
||||
{
|
||||
"hidden": false,
|
||||
"id": "autodate3332085495",
|
||||
"name": "updated",
|
||||
"onCreate": true,
|
||||
"onUpdate": true,
|
||||
"presentable": false,
|
||||
"system": false,
|
||||
"type": "autodate"
|
||||
}
|
||||
],
|
||||
"id": "pbc_3525142174",
|
||||
"indexes": [],
|
||||
"listRule": "user = @request.auth.id",
|
||||
"name": "api_tokens",
|
||||
"system": false,
|
||||
"type": "base",
|
||||
"updateRule": null,
|
||||
"viewRule": "user = @request.auth.id"
|
||||
}`
|
||||
|
||||
collection := &core.Collection{}
|
||||
if err := json.Unmarshal([]byte(jsonData), &collection); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return app.Save(collection)
|
||||
}, func(app core.App) error {
|
||||
collection, err := app.FindCollectionByNameOrId("pbc_3525142174")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return app.Delete(collection)
|
||||
})
|
||||
}
|
||||
178
db/migrations/1772400001_created_trail_external_reference.go
Normal file
178
db/migrations/1772400001_created_trail_external_reference.go
Normal file
@@ -0,0 +1,178 @@
|
||||
package migrations
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/pocketbase/dbx"
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
m "github.com/pocketbase/pocketbase/migrations"
|
||||
)
|
||||
|
||||
func init() {
|
||||
m.Register(func(app core.App) error {
|
||||
jsonData := `{
|
||||
"createRule": null,
|
||||
"deleteRule": null,
|
||||
"fields": [
|
||||
{
|
||||
"autogeneratePattern": "[a-z0-9]{15}",
|
||||
"hidden": false,
|
||||
"id": "text3208210256",
|
||||
"max": 15,
|
||||
"min": 15,
|
||||
"name": "id",
|
||||
"pattern": "^[a-z0-9]+$",
|
||||
"presentable": false,
|
||||
"primaryKey": true,
|
||||
"required": true,
|
||||
"system": true,
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"cascadeDelete": true,
|
||||
"collectionId": "e864strfxo14pm4",
|
||||
"hidden": false,
|
||||
"id": "relation420001001",
|
||||
"maxSelect": 1,
|
||||
"minSelect": 0,
|
||||
"name": "trail",
|
||||
"presentable": false,
|
||||
"required": true,
|
||||
"system": false,
|
||||
"type": "relation"
|
||||
},
|
||||
{
|
||||
"hidden": false,
|
||||
"id": "select420001002",
|
||||
"maxSelect": 1,
|
||||
"name": "provider",
|
||||
"presentable": false,
|
||||
"required": true,
|
||||
"system": false,
|
||||
"type": "select",
|
||||
"values": [
|
||||
"strava",
|
||||
"komoot",
|
||||
"hammerhead"
|
||||
]
|
||||
},
|
||||
{
|
||||
"autogeneratePattern": "",
|
||||
"hidden": false,
|
||||
"id": "text420001003",
|
||||
"max": 255,
|
||||
"min": 1,
|
||||
"name": "external_id",
|
||||
"pattern": "",
|
||||
"presentable": false,
|
||||
"primaryKey": false,
|
||||
"required": true,
|
||||
"system": false,
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"hidden": false,
|
||||
"id": "autodate420001004",
|
||||
"name": "created",
|
||||
"onCreate": true,
|
||||
"onUpdate": false,
|
||||
"presentable": false,
|
||||
"system": false,
|
||||
"type": "autodate"
|
||||
},
|
||||
{
|
||||
"hidden": false,
|
||||
"id": "autodate420001005",
|
||||
"name": "updated",
|
||||
"onCreate": true,
|
||||
"onUpdate": true,
|
||||
"presentable": false,
|
||||
"system": false,
|
||||
"type": "autodate"
|
||||
}
|
||||
],
|
||||
"id": "pbc_420001000",
|
||||
"indexes": [
|
||||
"CREATE UNIQUE INDEX ` + "`" + `idx_trail_external_reference_provider_external_id` + "`" + ` ON ` + "`" + `trail_external_reference` + "`" + ` (` + "`" + `provider` + "`" + `, ` + "`" + `external_id` + "`" + `)",
|
||||
"CREATE UNIQUE INDEX ` + "`" + `idx_trail_external_reference_trail_provider_external_id` + "`" + ` ON ` + "`" + `trail_external_reference` + "`" + ` (` + "`" + `trail` + "`" + `, ` + "`" + `provider` + "`" + `, ` + "`" + `external_id` + "`" + `)"
|
||||
],
|
||||
"listRule": null,
|
||||
"name": "trail_external_reference",
|
||||
"system": false,
|
||||
"type": "base",
|
||||
"updateRule": null,
|
||||
"viewRule": null
|
||||
}`
|
||||
|
||||
collection := &core.Collection{}
|
||||
if err := json.Unmarshal([]byte(jsonData), &collection); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := app.Save(collection); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
referenceCollection, err := app.FindCollectionByNameOrId("trail_external_reference")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
trails, err := app.FindRecordsByFilter(
|
||||
"trails",
|
||||
"external_provider != '' && external_id != ''",
|
||||
"",
|
||||
-1,
|
||||
0,
|
||||
nil,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, trail := range trails {
|
||||
provider := trail.GetString("external_provider")
|
||||
externalID := trail.GetString("external_id")
|
||||
if provider == "" || externalID == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
existing, err := app.FindRecordsByFilter(
|
||||
"trail_external_reference",
|
||||
"provider={:provider} && external_id={:external_id}",
|
||||
"",
|
||||
1,
|
||||
0,
|
||||
dbx.Params{
|
||||
"provider": provider,
|
||||
"external_id": externalID,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(existing) > 0 {
|
||||
app.Logger().Warn("Skipping duplicate trail external reference during migration", "provider", provider, "external_id", externalID, "trail", trail.Id)
|
||||
continue
|
||||
}
|
||||
|
||||
record := core.NewRecord(referenceCollection)
|
||||
record.Load(map[string]any{
|
||||
"trail": trail.Id,
|
||||
"provider": provider,
|
||||
"external_id": externalID,
|
||||
})
|
||||
if err := app.Save(record); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}, func(app core.App) error {
|
||||
collection, err := app.FindCollectionByNameOrId("pbc_420001000")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return app.Delete(collection)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
package migrations
|
||||
|
||||
import (
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
m "github.com/pocketbase/pocketbase/migrations"
|
||||
)
|
||||
|
||||
func init() {
|
||||
m.Register(func(app core.App) error {
|
||||
collection, err := app.FindCollectionByNameOrId("e864strfxo14pm4")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
collection.Fields.RemoveById("sajmiuau")
|
||||
collection.Fields.RemoveById("htr35nha")
|
||||
|
||||
return app.Save(collection)
|
||||
}, func(app core.App) error {
|
||||
collection, err := app.FindCollectionByNameOrId("e864strfxo14pm4")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := collection.Fields.AddMarshaledJSONAt(17, []byte(`{
|
||||
"autogeneratePattern": "",
|
||||
"hidden": false,
|
||||
"id": "sajmiuau",
|
||||
"max": 0,
|
||||
"min": 0,
|
||||
"name": "external_id",
|
||||
"pattern": "",
|
||||
"presentable": false,
|
||||
"primaryKey": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "text"
|
||||
}`)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := collection.Fields.AddMarshaledJSONAt(18, []byte(`{
|
||||
"hidden": false,
|
||||
"id": "htr35nha",
|
||||
"maxSelect": 1,
|
||||
"name": "external_provider",
|
||||
"presentable": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "select",
|
||||
"values": [
|
||||
"strava",
|
||||
"komoot",
|
||||
"hammerhead"
|
||||
]
|
||||
}`)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return app.Save(collection)
|
||||
})
|
||||
}
|
||||
60
db/migrations/1775994551_updated_waypoints.go
Normal file
60
db/migrations/1775994551_updated_waypoints.go
Normal file
@@ -0,0 +1,60 @@
|
||||
package migrations
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
m "github.com/pocketbase/pocketbase/migrations"
|
||||
)
|
||||
|
||||
func init() {
|
||||
m.Register(func(app core.App) error {
|
||||
collection, err := app.FindCollectionByNameOrId("goeo2ubp103rzp9")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// add field
|
||||
if err := collection.Fields.AddMarshaledJSONAt(8, []byte(`{
|
||||
"exceptDomains": null,
|
||||
"hidden": false,
|
||||
"id": "url2434853685",
|
||||
"name": "iri",
|
||||
"onlyDomains": null,
|
||||
"presentable": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "url"
|
||||
}`)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// update collection data
|
||||
if err := json.Unmarshal([]byte(`{
|
||||
"indexes": [
|
||||
"CREATE UNIQUE INDEX `+"`"+`idx_GgX6MsdCJq`+"`"+` ON `+"`"+`waypoints`+"`"+` (`+"`"+`iri`+"`"+`) WHERE iri IS NOT NULL AND iri != \"\";"
|
||||
]
|
||||
}`), &collection); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return app.Save(collection)
|
||||
}, func(app core.App) error {
|
||||
collection, err := app.FindCollectionByNameOrId("goeo2ubp103rzp9")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// update collection data
|
||||
if err := json.Unmarshal([]byte(`{
|
||||
"indexes": []
|
||||
}`), &collection); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// remove field
|
||||
collection.Fields.RemoveById("url2434853685")
|
||||
|
||||
return app.Save(collection)
|
||||
})
|
||||
}
|
||||
78
db/migrations/1775997520_updated_waypoints.go
Normal file
78
db/migrations/1775997520_updated_waypoints.go
Normal file
@@ -0,0 +1,78 @@
|
||||
package migrations
|
||||
|
||||
import (
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
m "github.com/pocketbase/pocketbase/migrations"
|
||||
)
|
||||
|
||||
func init() {
|
||||
m.Register(func(app core.App) error {
|
||||
collection, err := app.FindCollectionByNameOrId("goeo2ubp103rzp9")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// add field
|
||||
if err := collection.Fields.AddMarshaledJSONAt(11, []byte(`{
|
||||
"cascadeDelete": true,
|
||||
"collectionId": "pbc_1295301207",
|
||||
"hidden": false,
|
||||
"id": "relation3182418120",
|
||||
"maxSelect": 1,
|
||||
"minSelect": 0,
|
||||
"name": "author",
|
||||
"presentable": false,
|
||||
"required": true,
|
||||
"system": false,
|
||||
"type": "relation"
|
||||
}`)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// update field
|
||||
if err := collection.Fields.AddMarshaledJSONAt(9, []byte(`{
|
||||
"cascadeDelete": true,
|
||||
"collectionId": "_pb_users_auth_",
|
||||
"hidden": false,
|
||||
"id": "8qbxrsd8",
|
||||
"maxSelect": 1,
|
||||
"minSelect": 0,
|
||||
"name": "user",
|
||||
"presentable": false,
|
||||
"required": true,
|
||||
"system": false,
|
||||
"type": "relation"
|
||||
}`)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return app.Save(collection)
|
||||
}, func(app core.App) error {
|
||||
collection, err := app.FindCollectionByNameOrId("goeo2ubp103rzp9")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// remove field
|
||||
collection.Fields.RemoveById("relation3182418120")
|
||||
|
||||
// update field
|
||||
if err := collection.Fields.AddMarshaledJSONAt(9, []byte(`{
|
||||
"cascadeDelete": true,
|
||||
"collectionId": "_pb_users_auth_",
|
||||
"hidden": false,
|
||||
"id": "8qbxrsd8",
|
||||
"maxSelect": 1,
|
||||
"minSelect": 0,
|
||||
"name": "author",
|
||||
"presentable": false,
|
||||
"required": true,
|
||||
"system": false,
|
||||
"type": "relation"
|
||||
}`)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return app.Save(collection)
|
||||
})
|
||||
}
|
||||
33
db/migrations/1775997935_set_waypoint_authors.go
Normal file
33
db/migrations/1775997935_set_waypoint_authors.go
Normal file
@@ -0,0 +1,33 @@
|
||||
package migrations
|
||||
|
||||
import (
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
m "github.com/pocketbase/pocketbase/migrations"
|
||||
)
|
||||
|
||||
func init() {
|
||||
m.Register(func(app core.App) error {
|
||||
wps, err := app.FindAllRecords("waypoints")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, wp := range wps {
|
||||
actor, err := app.FindFirstRecordByData("activitypub_actors", "user", wp.GetString("user"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
wp.Set("author", actor.Id)
|
||||
err = app.UnsafeWithoutHooks().Save(wp)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}, func(app core.App) error {
|
||||
// add down queries...
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
44
db/migrations/1775997936_updated_waypoints.go
Normal file
44
db/migrations/1775997936_updated_waypoints.go
Normal file
@@ -0,0 +1,44 @@
|
||||
package migrations
|
||||
|
||||
import (
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
m "github.com/pocketbase/pocketbase/migrations"
|
||||
)
|
||||
|
||||
func init() {
|
||||
m.Register(func(app core.App) error {
|
||||
collection, err := app.FindCollectionByNameOrId("goeo2ubp103rzp9")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// remove field
|
||||
collection.Fields.RemoveById("8qbxrsd8")
|
||||
|
||||
return app.Save(collection)
|
||||
}, func(app core.App) error {
|
||||
collection, err := app.FindCollectionByNameOrId("goeo2ubp103rzp9")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// add field
|
||||
if err := collection.Fields.AddMarshaledJSONAt(9, []byte(`{
|
||||
"cascadeDelete": true,
|
||||
"collectionId": "_pb_users_auth_",
|
||||
"hidden": false,
|
||||
"id": "8qbxrsd8",
|
||||
"maxSelect": 1,
|
||||
"minSelect": 0,
|
||||
"name": "user",
|
||||
"presentable": false,
|
||||
"required": true,
|
||||
"system": false,
|
||||
"type": "relation"
|
||||
}`)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return app.Save(collection)
|
||||
})
|
||||
}
|
||||
40
db/migrations/1776170099_updated_trails.go
Normal file
40
db/migrations/1776170099_updated_trails.go
Normal file
@@ -0,0 +1,40 @@
|
||||
package migrations
|
||||
|
||||
import (
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
m "github.com/pocketbase/pocketbase/migrations"
|
||||
)
|
||||
|
||||
func init() {
|
||||
m.Register(func(app core.App) error {
|
||||
collection, err := app.FindCollectionByNameOrId("e864strfxo14pm4")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// add field
|
||||
if err := collection.Fields.AddMarshaledJSONAt(23, []byte(`{
|
||||
"hidden": false,
|
||||
"id": "bool678597678",
|
||||
"name": "needs_full_sync",
|
||||
"presentable": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "bool"
|
||||
}`)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return app.Save(collection)
|
||||
}, func(app core.App) error {
|
||||
collection, err := app.FindCollectionByNameOrId("e864strfxo14pm4")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// remove field
|
||||
collection.Fields.RemoveById("bool678597678")
|
||||
|
||||
return app.Save(collection)
|
||||
})
|
||||
}
|
||||
42
db/migrations/1776243566_updated_trails.go
Normal file
42
db/migrations/1776243566_updated_trails.go
Normal file
@@ -0,0 +1,42 @@
|
||||
package migrations
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
m "github.com/pocketbase/pocketbase/migrations"
|
||||
)
|
||||
|
||||
func init() {
|
||||
m.Register(func(app core.App) error {
|
||||
collection, err := app.FindCollectionByNameOrId("e864strfxo14pm4")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// update collection data
|
||||
if err := json.Unmarshal([]byte(`{
|
||||
"createRule": "@request.auth.id != \"\" && (@request.body.author.user = @request.auth.id)",
|
||||
"updateRule": "author.user = @request.auth.id || (@request.auth.id != \"\" && trail_share_via_trail.trail = id && trail_share_via_trail.actor.user ?= @request.auth.id && trail_share_via_trail.permission = \"edit\")"
|
||||
}`), &collection); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return app.Save(collection)
|
||||
}, func(app core.App) error {
|
||||
collection, err := app.FindCollectionByNameOrId("e864strfxo14pm4")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// update collection data
|
||||
if err := json.Unmarshal([]byte(`{
|
||||
"createRule": "@request.auth.id != \"\" && (@request.body.author.user = @request.auth.id || author.isLocal = false)",
|
||||
"updateRule": "author.user = @request.auth.id || (@request.auth.id != \"\" && trail_share_via_trail.trail = id && trail_share_via_trail.actor.user ?= @request.auth.id && trail_share_via_trail.permission = \"edit\") || (@request.auth.id != \"\" && author.isLocal = false)"
|
||||
}`), &collection); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return app.Save(collection)
|
||||
})
|
||||
}
|
||||
42
db/migrations/1776243589_updated_lists.go
Normal file
42
db/migrations/1776243589_updated_lists.go
Normal file
@@ -0,0 +1,42 @@
|
||||
package migrations
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
m "github.com/pocketbase/pocketbase/migrations"
|
||||
)
|
||||
|
||||
func init() {
|
||||
m.Register(func(app core.App) error {
|
||||
collection, err := app.FindCollectionByNameOrId("r6gu2ajyidy1x69")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// update collection data
|
||||
if err := json.Unmarshal([]byte(`{
|
||||
"createRule": "@request.auth.id != \"\" && (@request.body.author.user = @request.auth.id)",
|
||||
"updateRule": "author.user = @request.auth.id || (@request.auth.id != \"\" && list_share_via_list.list = id && list_share_via_list.actor.user ?= @request.auth.id && list_share_via_list.permission = \"edit\")"
|
||||
}`), &collection); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return app.Save(collection)
|
||||
}, func(app core.App) error {
|
||||
collection, err := app.FindCollectionByNameOrId("r6gu2ajyidy1x69")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// update collection data
|
||||
if err := json.Unmarshal([]byte(`{
|
||||
"createRule": "@request.auth.id != \"\" && (@request.body.author.user = @request.auth.id || author.isLocal = false)",
|
||||
"updateRule": "author.user = @request.auth.id || (@request.auth.id != \"\" && list_share_via_list.list = id && list_share_via_list.actor.user ?= @request.auth.id && list_share_via_list.permission = \"edit\") || (@request.auth.id != \"\" && author.isLocal = false)"
|
||||
}`), &collection); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return app.Save(collection)
|
||||
})
|
||||
}
|
||||
62
db/migrations/1776246553_updated_trails.go
Normal file
62
db/migrations/1776246553_updated_trails.go
Normal file
@@ -0,0 +1,62 @@
|
||||
package migrations
|
||||
|
||||
import (
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
m "github.com/pocketbase/pocketbase/migrations"
|
||||
)
|
||||
|
||||
func init() {
|
||||
m.Register(func(app core.App) error {
|
||||
collection, err := app.FindCollectionByNameOrId("e864strfxo14pm4")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// add field
|
||||
if err := collection.Fields.AddMarshaledJSONAt(5, []byte(`{
|
||||
"hidden": false,
|
||||
"id": "bool989355118",
|
||||
"name": "completed",
|
||||
"presentable": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "bool"
|
||||
}`)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = app.Save(collection)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
logs, err := app.FindAllRecords("summit_logs")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, l := range logs {
|
||||
trail, err := app.FindRecordById("trails", l.GetString("trail"))
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
trail.Set("completed", true)
|
||||
err = app.Save(trail)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}, func(app core.App) error {
|
||||
collection, err := app.FindCollectionByNameOrId("e864strfxo14pm4")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// remove field
|
||||
collection.Fields.RemoveById("bool989355118")
|
||||
|
||||
return app.Save(collection)
|
||||
})
|
||||
}
|
||||
88
db/migrations/1776675228_updated_activitypub_actors.go
Normal file
88
db/migrations/1776675228_updated_activitypub_actors.go
Normal file
@@ -0,0 +1,88 @@
|
||||
package migrations
|
||||
|
||||
import (
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
m "github.com/pocketbase/pocketbase/migrations"
|
||||
)
|
||||
|
||||
func init() {
|
||||
m.Register(func(app core.App) error {
|
||||
collection, err := app.FindCollectionByNameOrId("pbc_1295301207")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// update field
|
||||
if err := collection.Fields.AddMarshaledJSONAt(5, []byte(`{
|
||||
"hidden": false,
|
||||
"id": "number1386272118",
|
||||
"max": null,
|
||||
"min": null,
|
||||
"name": "follower_count",
|
||||
"onlyInt": true,
|
||||
"presentable": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "number"
|
||||
}`)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// update field
|
||||
if err := collection.Fields.AddMarshaledJSONAt(6, []byte(`{
|
||||
"hidden": false,
|
||||
"id": "number3430500629",
|
||||
"max": null,
|
||||
"min": null,
|
||||
"name": "following_count",
|
||||
"onlyInt": true,
|
||||
"presentable": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "number"
|
||||
}`)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return app.Save(collection)
|
||||
}, func(app core.App) error {
|
||||
collection, err := app.FindCollectionByNameOrId("pbc_1295301207")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// update field
|
||||
if err := collection.Fields.AddMarshaledJSONAt(5, []byte(`{
|
||||
"hidden": false,
|
||||
"id": "number1386272118",
|
||||
"max": null,
|
||||
"min": null,
|
||||
"name": "followerCount",
|
||||
"onlyInt": true,
|
||||
"presentable": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "number"
|
||||
}`)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// update field
|
||||
if err := collection.Fields.AddMarshaledJSONAt(6, []byte(`{
|
||||
"hidden": false,
|
||||
"id": "number3430500629",
|
||||
"max": null,
|
||||
"min": null,
|
||||
"name": "followingCount",
|
||||
"onlyInt": true,
|
||||
"presentable": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "number"
|
||||
}`)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return app.Save(collection)
|
||||
})
|
||||
}
|
||||
40
db/migrations/1778145631_updated_lists.go
Normal file
40
db/migrations/1778145631_updated_lists.go
Normal file
@@ -0,0 +1,40 @@
|
||||
package migrations
|
||||
|
||||
import (
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
m "github.com/pocketbase/pocketbase/migrations"
|
||||
)
|
||||
|
||||
func init() {
|
||||
m.Register(func(app core.App) error {
|
||||
collection, err := app.FindCollectionByNameOrId("r6gu2ajyidy1x69")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// add field
|
||||
if err := collection.Fields.AddMarshaledJSONAt(8, []byte(`{
|
||||
"hidden": false,
|
||||
"id": "bool678597678",
|
||||
"name": "needs_full_sync",
|
||||
"presentable": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "bool"
|
||||
}`)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return app.Save(collection)
|
||||
}, func(app core.App) error {
|
||||
collection, err := app.FindCollectionByNameOrId("r6gu2ajyidy1x69")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// remove field
|
||||
collection.Fields.RemoveById("bool678597678")
|
||||
|
||||
return app.Save(collection)
|
||||
})
|
||||
}
|
||||
42
db/migrations/1778490903_updated_comments.go
Normal file
42
db/migrations/1778490903_updated_comments.go
Normal file
@@ -0,0 +1,42 @@
|
||||
package migrations
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
m "github.com/pocketbase/pocketbase/migrations"
|
||||
)
|
||||
|
||||
func init() {
|
||||
m.Register(func(app core.App) error {
|
||||
collection, err := app.FindCollectionByNameOrId("lf06qip3f4d11yk")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// update collection data
|
||||
if err := json.Unmarshal([]byte(`{
|
||||
"listRule": "((@request.auth.id != \"\" && trail.author.user = @request.auth.id) || trail.public = true) || author.user = @request.auth.id || trail.trail_share_via_trail.actor.user ?= @request.auth.id",
|
||||
"viewRule": "((@request.auth.id != \"\" && trail.author.user = @request.auth.id) || trail.public = true) || author.user = @request.auth.id || trail.trail_share_via_trail.actor.user ?= @request.auth.id"
|
||||
}`), &collection); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return app.Save(collection)
|
||||
}, func(app core.App) error {
|
||||
collection, err := app.FindCollectionByNameOrId("lf06qip3f4d11yk")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// update collection data
|
||||
if err := json.Unmarshal([]byte(`{
|
||||
"listRule": "((@request.auth.id != \"\" && trail.author.user = @request.auth.id) || trail.public = true) || author = @request.auth.id || trail.trail_share_via_trail.actor.user ?= @request.auth.id",
|
||||
"viewRule": "((@request.auth.id != \"\" && trail.author.user = @request.auth.id) || trail.public = true) || author = @request.auth.id || trail.trail_share_via_trail.actor.user ?= @request.auth.id"
|
||||
}`), &collection); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return app.Save(collection)
|
||||
})
|
||||
}
|
||||
49
db/migrations/1778583029_updated_activitypub_actors.go
Normal file
49
db/migrations/1778583029_updated_activitypub_actors.go
Normal file
@@ -0,0 +1,49 @@
|
||||
package migrations
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
m "github.com/pocketbase/pocketbase/migrations"
|
||||
)
|
||||
|
||||
func init() {
|
||||
m.Register(func(app core.App) error {
|
||||
collection, err := app.FindCollectionByNameOrId("pbc_1295301207")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// update collection data
|
||||
if err := json.Unmarshal([]byte(`{
|
||||
"indexes": [
|
||||
"CREATE UNIQUE INDEX `+"`"+`idx_rpT7QJwWTm`+"`"+` ON `+"`"+`activitypub_actors`+"`"+` (`+"`"+`iri`+"`"+`)",
|
||||
"CREATE UNIQUE INDEX `+"`"+`idx_actors_username_domain`+"`"+` ON `+"`"+`activitypub_actors`+"`"+` (\n `+"`"+`preferred_username`+"`"+`,\n `+"`"+`domain`+"`"+`\n)",
|
||||
"CREATE INDEX idx_activitypub_actors_user ON activitypub_actors(user) WHERE user IS NOT null;",
|
||||
"CREATE INDEX `+"`"+`idx_x8xyfe8q8y`+"`"+` ON `+"`"+`activitypub_actors`+"`"+` (`+"`"+`inbox`+"`"+`)"
|
||||
]
|
||||
}`), &collection); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return app.Save(collection)
|
||||
}, func(app core.App) error {
|
||||
collection, err := app.FindCollectionByNameOrId("pbc_1295301207")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// update collection data
|
||||
if err := json.Unmarshal([]byte(`{
|
||||
"indexes": [
|
||||
"CREATE UNIQUE INDEX `+"`"+`idx_rpT7QJwWTm`+"`"+` ON `+"`"+`activitypub_actors`+"`"+` (`+"`"+`iri`+"`"+`)",
|
||||
"CREATE INDEX idx_actors_username_domain\nON activitypub_actors(preferred_username, domain);",
|
||||
"CREATE INDEX idx_activitypub_actors_user ON activitypub_actors(user);"
|
||||
]
|
||||
}`), &collection); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return app.Save(collection)
|
||||
})
|
||||
}
|
||||
82
db/migrations/1778583207_updated_trails_filter.go
Normal file
82
db/migrations/1778583207_updated_trails_filter.go
Normal file
@@ -0,0 +1,82 @@
|
||||
package migrations
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
m "github.com/pocketbase/pocketbase/migrations"
|
||||
)
|
||||
|
||||
func init() {
|
||||
m.Register(func(app core.App) error {
|
||||
collection, err := app.FindCollectionByNameOrId("4wbv9tz5zjdrjh1")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// update collection data
|
||||
if err := json.Unmarshal([]byte(`{
|
||||
"viewQuery": "SELECT\n a.id,\n a.user,\n COALESCE(printf(\"%.2f\", MAX(t.max_distance)), 0) AS max_distance,\n COALESCE(printf(\"%.2f\", MAX(t.max_elevation_gain)), 0) AS max_elevation_gain,\n COALESCE(printf(\"%.2f\", MAX(t.max_elevation_loss)), 0) AS max_elevation_loss,\n COALESCE(printf(\"%.2f\", MAX(t.max_duration)), 0) AS max_duration,\n COALESCE(printf(\"%.2f\", MIN(t.min_distance)), 0) AS min_distance,\n COALESCE(printf(\"%.2f\", MIN(t.min_elevation_gain)), 0) AS min_elevation_gain,\n COALESCE(printf(\"%.2f\", MIN(t.min_elevation_loss)), 0) AS min_elevation_loss,\n COALESCE(printf(\"%.2f\", MIN(t.min_duration)), 0) AS min_duration\nFROM activitypub_actors a\nLEFT JOIN (\n SELECT author AS actor_id,\n MAX(distance) AS max_distance,\n MIN(distance) AS min_distance,\n MAX(elevation_gain) AS max_elevation_gain,\n MIN(elevation_gain) AS min_elevation_gain,\n MAX(elevation_loss) AS max_elevation_loss,\n MIN(elevation_loss) AS min_elevation_loss,\n MAX(duration) AS max_duration,\n MIN(duration) AS min_duration\n FROM trails\n GROUP BY author\n UNION ALL\n SELECT ts.actor AS actor_id,\n MAX(t.distance) AS max_distance,\n MIN(t.distance) AS min_distance,\n MAX(t.elevation_gain) AS max_elevation_gain,\n MIN(t.elevation_gain) AS min_elevation_gain,\n MAX(t.elevation_loss) AS max_elevation_loss,\n MIN(t.elevation_loss) AS min_elevation_loss,\n MAX(t.duration) AS max_duration,\n MIN(t.duration) AS min_duration\n FROM trail_share ts\n JOIN trails t ON t.id = ts.trail\n GROUP BY ts.actor\n UNION ALL\n SELECT a2.id AS actor_id,\n p.max_distance,\n p.min_distance,\n p.max_elevation_gain,\n p.min_elevation_gain,\n p.max_elevation_loss,\n p.min_elevation_loss,\n p.max_duration,\n p.min_duration\n FROM activitypub_actors a2\n CROSS JOIN (\n SELECT\n MAX(distance) AS max_distance,\n MIN(distance) AS min_distance,\n MAX(elevation_gain) AS max_elevation_gain,\n MIN(elevation_gain) AS min_elevation_gain,\n MAX(elevation_loss) AS max_elevation_loss,\n MIN(elevation_loss) AS min_elevation_loss,\n MAX(duration) AS max_duration,\n MIN(duration) AS min_duration\n FROM trails\n WHERE public = TRUE\n ) p\n) t ON t.actor_id = a.id\nWHERE a.user != \"\"\nGROUP BY a.id;"
|
||||
}`), &collection); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// remove field
|
||||
collection.Fields.RemoveById("_clone_rQGp")
|
||||
|
||||
// add field
|
||||
if err := collection.Fields.AddMarshaledJSONAt(1, []byte(`{
|
||||
"cascadeDelete": true,
|
||||
"collectionId": "_pb_users_auth_",
|
||||
"help": "",
|
||||
"hidden": false,
|
||||
"id": "_clone_bYSp",
|
||||
"maxSelect": 1,
|
||||
"minSelect": 0,
|
||||
"name": "user",
|
||||
"presentable": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "relation"
|
||||
}`)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return app.Save(collection)
|
||||
}, func(app core.App) error {
|
||||
collection, err := app.FindCollectionByNameOrId("4wbv9tz5zjdrjh1")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// update collection data
|
||||
if err := json.Unmarshal([]byte(`{
|
||||
"viewQuery": "SELECT\n a.id,\n a.user,\n COALESCE(printf(\"%.2f\", MAX(t.max_distance)), 0) AS max_distance,\n COALESCE(printf(\"%.2f\", MAX(t.max_elevation_gain)), 0) AS max_elevation_gain,\n COALESCE(printf(\"%.2f\", MAX(t.max_elevation_loss)), 0) AS max_elevation_loss,\n COALESCE(printf(\"%.2f\", MAX(t.max_duration)), 0) AS max_duration,\n COALESCE(printf(\"%.2f\", MIN(t.min_distance)), 0) AS min_distance,\n COALESCE(printf(\"%.2f\", MIN(t.min_elevation_gain)), 0) AS min_elevation_gain,\n COALESCE(printf(\"%.2f\", MIN(t.min_elevation_loss)), 0) AS min_elevation_loss,\n COALESCE(printf(\"%.2f\", MIN(t.min_duration)), 0) AS min_duration\nFROM activitypub_actors a\nLEFT JOIN (\n SELECT author AS actor_id,\n MAX(distance) AS max_distance,\n MIN(distance) AS min_distance,\n MAX(elevation_gain) AS max_elevation_gain,\n MIN(elevation_gain) AS min_elevation_gain,\n MAX(elevation_loss) AS max_elevation_loss,\n MIN(elevation_loss) AS min_elevation_loss,\n MAX(duration) AS max_duration,\n MIN(duration) AS min_duration\n FROM trails\n GROUP BY author\n UNION ALL\n SELECT ts.actor AS actor_id,\n MAX(t.distance) AS max_distance,\n MIN(t.distance) AS min_distance,\n MAX(t.elevation_gain) AS max_elevation_gain,\n MIN(t.elevation_gain) AS min_elevation_gain,\n MAX(t.elevation_loss) AS max_elevation_loss,\n MIN(t.elevation_loss) AS min_elevation_loss,\n MAX(t.duration) AS max_duration,\n MIN(t.duration) AS min_duration\n FROM trail_share ts\n JOIN trails t ON t.id = ts.trail\n GROUP BY ts.actor\n UNION ALL\n SELECT a2.id AS actor_id,\n p.max_distance,\n p.min_distance,\n p.max_elevation_gain,\n p.min_elevation_gain,\n p.max_elevation_loss,\n p.min_elevation_loss,\n p.max_duration,\n p.min_duration\n FROM activitypub_actors a2\n CROSS JOIN (\n SELECT\n MAX(distance) AS max_distance,\n MIN(distance) AS min_distance,\n MAX(elevation_gain) AS max_elevation_gain,\n MIN(elevation_gain) AS min_elevation_gain,\n MAX(elevation_loss) AS max_elevation_loss,\n MIN(elevation_loss) AS min_elevation_loss,\n MAX(duration) AS max_duration,\n MIN(duration) AS min_duration\n FROM trails\n WHERE public = TRUE\n ) p\n) t ON t.actor_id = a.id\nWHERE a.user is not null\nGROUP BY a.id;"
|
||||
}`), &collection); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// add field
|
||||
if err := collection.Fields.AddMarshaledJSONAt(1, []byte(`{
|
||||
"cascadeDelete": true,
|
||||
"collectionId": "_pb_users_auth_",
|
||||
"help": "",
|
||||
"hidden": false,
|
||||
"id": "_clone_rQGp",
|
||||
"maxSelect": 1,
|
||||
"minSelect": 0,
|
||||
"name": "user",
|
||||
"presentable": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "relation"
|
||||
}`)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// remove field
|
||||
collection.Fields.RemoveById("_clone_bYSp")
|
||||
|
||||
return app.Save(collection)
|
||||
})
|
||||
}
|
||||
82
db/migrations/1778583263_updated_trails_bounding_box.go
Normal file
82
db/migrations/1778583263_updated_trails_bounding_box.go
Normal file
@@ -0,0 +1,82 @@
|
||||
package migrations
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
m "github.com/pocketbase/pocketbase/migrations"
|
||||
)
|
||||
|
||||
func init() {
|
||||
m.Register(func(app core.App) error {
|
||||
collection, err := app.FindCollectionByNameOrId("urytyc428mwlbqq")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// update collection data
|
||||
if err := json.Unmarshal([]byte(`{
|
||||
"viewQuery": "SELECT \n a.id, a.user, \n COALESCE(MAX(t.max_lat), 0) AS max_lat, \n COALESCE(MAX(t.max_lon), 0) AS max_lon, \n COALESCE(MIN(t.min_lat), 0) AS min_lat, \n COALESCE(MIN(t.min_lon), 0) AS min_lon \nFROM activitypub_actors a \nLEFT JOIN ( \n SELECT author AS actor_id, \n MAX(lat) AS max_lat, \n MAX(lon) AS max_lon, \n MIN(lat) AS min_lat, \n MIN(lon) AS min_lon \n FROM trails \n GROUP BY author \n UNION ALL \n SELECT ts.actor AS actor_id, \n MAX(t.lat) AS max_lat, \n MAX(t.lon) AS max_lon, \n MIN(t.lat) AS min_lat, \n MIN(t.lon) AS min_lon \n FROM trail_share ts \n JOIN trails t ON t.id = ts.trail \n GROUP BY ts.actor \n UNION ALL \n SELECT a2.id AS actor_id, \n p.max_lat, p.max_lon, p.min_lat, p.min_lon \n FROM activitypub_actors a2 \n CROSS JOIN ( \n SELECT \n MAX(lat) AS max_lat, \n MAX(lon) AS max_lon, \n MIN(lat) AS min_lat, \n MIN(lon) AS min_lon \n FROM trails \n WHERE public = TRUE \n ) p \n) t ON t.actor_id = a.id \nWHERE a.user != \"\"\nGROUP BY a.id;"
|
||||
}`), &collection); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// remove field
|
||||
collection.Fields.RemoveById("_clone_MXPc")
|
||||
|
||||
// add field
|
||||
if err := collection.Fields.AddMarshaledJSONAt(1, []byte(`{
|
||||
"cascadeDelete": true,
|
||||
"collectionId": "_pb_users_auth_",
|
||||
"help": "",
|
||||
"hidden": false,
|
||||
"id": "_clone_b2Wa",
|
||||
"maxSelect": 1,
|
||||
"minSelect": 0,
|
||||
"name": "user",
|
||||
"presentable": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "relation"
|
||||
}`)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return app.Save(collection)
|
||||
}, func(app core.App) error {
|
||||
collection, err := app.FindCollectionByNameOrId("urytyc428mwlbqq")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// update collection data
|
||||
if err := json.Unmarshal([]byte(`{
|
||||
"viewQuery": "SELECT \n a.id, a.user, \n COALESCE(MAX(t.max_lat), 0) AS max_lat, \n COALESCE(MAX(t.max_lon), 0) AS max_lon, \n COALESCE(MIN(t.min_lat), 0) AS min_lat, \n COALESCE(MIN(t.min_lon), 0) AS min_lon \nFROM activitypub_actors a \nLEFT JOIN ( \n SELECT author AS actor_id, \n MAX(lat) AS max_lat, \n MAX(lon) AS max_lon, \n MIN(lat) AS min_lat, \n MIN(lon) AS min_lon \n FROM trails \n GROUP BY author \n UNION ALL \n SELECT ts.actor AS actor_id, \n MAX(t.lat) AS max_lat, \n MAX(t.lon) AS max_lon, \n MIN(t.lat) AS min_lat, \n MIN(t.lon) AS min_lon \n FROM trail_share ts \n JOIN trails t ON t.id = ts.trail \n GROUP BY ts.actor \n UNION ALL \n SELECT a2.id AS actor_id, \n p.max_lat, p.max_lon, p.min_lat, p.min_lon \n FROM activitypub_actors a2 \n CROSS JOIN ( \n SELECT \n MAX(lat) AS max_lat, \n MAX(lon) AS max_lon, \n MIN(lat) AS min_lat, \n MIN(lon) AS min_lon \n FROM trails \n WHERE public = TRUE \n ) p \n) t ON t.actor_id = a.id \nGROUP BY a.id;"
|
||||
}`), &collection); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// add field
|
||||
if err := collection.Fields.AddMarshaledJSONAt(1, []byte(`{
|
||||
"cascadeDelete": true,
|
||||
"collectionId": "_pb_users_auth_",
|
||||
"help": "",
|
||||
"hidden": false,
|
||||
"id": "_clone_MXPc",
|
||||
"maxSelect": 1,
|
||||
"minSelect": 0,
|
||||
"name": "user",
|
||||
"presentable": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "relation"
|
||||
}`)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// remove field
|
||||
collection.Fields.RemoveById("_clone_b2Wa")
|
||||
|
||||
return app.Save(collection)
|
||||
})
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user